diff --git a/src/ImageSharp/DefaultInternalImageProcessorContext.cs b/src/ImageSharp/DefaultInternalImageProcessorContext.cs index 5aba9ae69..7ccc65e27 100644 --- a/src/ImageSharp/DefaultInternalImageProcessorContext.cs +++ b/src/ImageSharp/DefaultInternalImageProcessorContext.cs @@ -51,6 +51,9 @@ namespace SixLabors.ImageSharp return this.destination; } + /// + public Size GetCurrentSize() => this.GetCurrentBounds().Size; + /// public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle) { @@ -75,7 +78,12 @@ namespace SixLabors.ImageSharp /// public IImageProcessingContext ApplyProcessor(IImageProcessor processor) { - return this.ApplyProcessor(processor, this.source.Bounds()); + return this.ApplyProcessor(processor, this.GetCurrentBounds()); + } + + private Rectangle GetCurrentBounds() + { + return this.destination?.Bounds() ?? this.source.Bounds(); } } } \ No newline at end of file diff --git a/src/ImageSharp/IImageProcessingContext{TPixel}.cs b/src/ImageSharp/IImageProcessingContext{TPixel}.cs index b73337905..0e8efde3b 100644 --- a/src/ImageSharp/IImageProcessingContext{TPixel}.cs +++ b/src/ImageSharp/IImageProcessingContext{TPixel}.cs @@ -21,6 +21,12 @@ namespace SixLabors.ImageSharp /// MemoryManager MemoryManager { get; } + /// + /// Gets the image dimensions at the current point in the processing pipeline. + /// + /// The + Size GetCurrentSize(); + /// /// Adds the processor to the current set of image operations to be applied. /// @@ -47,7 +53,7 @@ namespace SixLabors.ImageSharp /// /// Adds the processors to the current image /// - /// The current image or a new image depending on withere this is alloed to mutate the source image. + /// The current image or a new image depending on whether this is allowed to mutate the source image. Image Apply(); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs index 4672b2ad4..7257bd664 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs +++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs @@ -96,6 +96,7 @@ namespace SixLabors.ImageSharp.Processing /// /// This method is called before the process is applied to prepare the processor. + /// TODO: We should probably name this 'BeforeFrameApply' /// /// The source image. Cannot be null. /// The cloned/destination image. Cannot be null. @@ -108,6 +109,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Applies the process to the specified portion of the specified at the specified location /// and with the specified size. + /// TODO: We should probably name this 'ApplyToFrame' /// /// The source image. Cannot be null. /// The cloned/destination image. Cannot be null. @@ -117,6 +119,7 @@ namespace SixLabors.ImageSharp.Processing /// /// This method is called after the process is applied to prepare the processor. + /// TODO: We should probably name this 'AfterFrameApply' /// /// The source image. Cannot be null. /// The cloned/destination image. Cannot be null. diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 287ce9d41..ac11e0dfb 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -21,27 +21,6 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class AffineTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { - private Size targetDimensions; - - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - public AffineTransformProcessor(Matrix3x2 matrix) - : this(matrix, KnownResamplers.Bicubic) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) - : this(matrix, sampler, Size.Empty) - { - } - /// /// Initializes a new instance of the class. /// @@ -52,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors : base(sampler) { this.TransformMatrix = matrix; - this.targetDimensions = targetDimensions; + this.TargetDimensions = targetDimensions; } /// @@ -60,18 +39,17 @@ namespace SixLabors.ImageSharp.Processing.Processors /// 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) { - if (this.targetDimensions == Size.Empty) - { - // TODO: CreateDestination() should not modify the processors state! (kinda CQRS) - this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix); - } - // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(source.GetMemoryManager(), this.targetDimensions, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(source.GetMemoryManager(), this.TargetDimensions, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); @@ -84,8 +62,8 @@ namespace SixLabors.ImageSharp.Processing.Processors Rectangle sourceRectangle, Configuration configuration) { - int height = this.targetDimensions.Height; - int width = this.targetDimensions.Width; + int height = this.TargetDimensions.Height; + int width = this.TargetDimensions.Width; Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(0, 0, width, height); @@ -207,8 +185,8 @@ namespace SixLabors.ImageSharp.Processing.Processors var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels - Vector4 mupltiplied = vector.Premultiply(); - sum += mupltiplied * xWeight * yWeight; + Vector4 multiplied = vector.Premultiply(); + sum += multiplied * xWeight * yWeight; } } @@ -233,16 +211,5 @@ namespace SixLabors.ImageSharp.Processing.Processors { return this.TransformMatrix; } - - /// - /// Gets the bounding relative to the source for the given transformation matrix. - /// - /// The source rectangle. - /// The transformation matrix. - /// The - protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) - { - return sourceDimensions; - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs index c39311bc3..7f811eebc 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing.Processors protected override void BeforeImageApply(Image source, Rectangle sourceRectangle) { Orientation orientation = GetExifOrientation(source); - + Size size = sourceRectangle.Size; switch (orientation) { case Orientation.TopRight: @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors break; case Orientation.BottomRight: - new RotateProcessor((int)RotateType.Rotate180).Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate180, size).Apply(source, sourceRectangle); break; case Orientation.BottomLeft: @@ -35,21 +35,21 @@ namespace SixLabors.ImageSharp.Processing.Processors break; case Orientation.LeftTop: - new RotateProcessor((int)RotateType.Rotate90).Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate90, size).Apply(source, sourceRectangle); new FlipProcessor(FlipType.Horizontal).Apply(source, sourceRectangle); break; case Orientation.RightTop: - new RotateProcessor((int)RotateType.Rotate90).Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate90, size).Apply(source, sourceRectangle); break; case Orientation.RightBottom: new FlipProcessor(FlipType.Vertical).Apply(source, sourceRectangle); - new RotateProcessor((int)RotateType.Rotate270).Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate270, size).Apply(source, sourceRectangle); break; case Orientation.LeftBottom: - new RotateProcessor((int)RotateType.Rotate270).Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate270, size).Apply(source, sourceRectangle); break; case Orientation.Unknown: diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs index 1e24b7c28..6b8314d17 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs @@ -19,8 +19,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The transform matrix /// The sampler to perform the transform operation. - protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) - : base(matrix, sampler) + /// The source image size + protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) + : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) { } @@ -30,11 +31,10 @@ namespace SixLabors.ImageSharp.Processing.Processors return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); } - /// - protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) + private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) { var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix).Size; + return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs index 92a008d7a..081ea8461 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs @@ -19,8 +19,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The transform matrix /// The sampler to perform the transform operation. - protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) - : base(matrix, sampler) + /// The source image size + protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) + : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) { } @@ -30,11 +31,10 @@ namespace SixLabors.ImageSharp.Processing.Processors return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); } - /// - protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) + private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) { var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix).Size; + return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs index 37c76bdcf..c04014ae3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; +// TODO: Convert this into a cloning processor inheriting TransformProcessor once Anton's memory PR is merged namespace SixLabors.ImageSharp.Processing.Processors { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 458871cdc..e16cd9ee8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -22,27 +22,6 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class ProjectiveTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { - private Size targetDimensions; - - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - public ProjectiveTransformProcessor(Matrix4x4 matrix) - : this(matrix, KnownResamplers.Bicubic) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) - : this(matrix, sampler, Size.Empty) - { - } - /// /// Initializes a new instance of the class. /// @@ -53,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors : base(sampler) { this.TransformMatrix = matrix; - this.targetDimensions = targetDimensions; + this.TargetDimensions = targetDimensions; } /// @@ -61,21 +40,17 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public Matrix4x4 TransformMatrix { get; } + /// + /// Gets the target dimensions to constrain the transformed image to + /// + public Size TargetDimensions { get; } + /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - if (this.targetDimensions == Size.Empty) - { - // TODO: CreateDestination() should not modify the processors state! (kinda CQRS) - this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix); - } - // We will always be creating the clone even for mutate because we may need to resize the canvas - IEnumerable> frames = source.Frames.Select( - x => new ImageFrame( - source.GetMemoryManager(), - this.targetDimensions, - x.MetaData.Clone())); + IEnumerable> frames = + source.Frames.Select(x => new ImageFrame(source.GetMemoryManager(), this.TargetDimensions.Width, this.TargetDimensions.Height, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); @@ -84,8 +59,8 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { - int height = this.targetDimensions.Height; - int width = this.targetDimensions.Width; + int height = this.TargetDimensions.Height; + int width = this.TargetDimensions.Width; Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(0, 0, width, height); @@ -207,8 +182,8 @@ namespace SixLabors.ImageSharp.Processing.Processors var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels - Vector4 mupltiplied = vector.Premultiply(); - sum += mupltiplied * xWeight * yWeight; + Vector4 multiplied = vector.Premultiply(); + sum += multiplied * xWeight * yWeight; } } @@ -233,16 +208,5 @@ namespace SixLabors.ImageSharp.Processing.Processors { return this.TransformMatrix; } - - /// - /// Gets the bounding relative to the source for the given transformation matrix. - /// - /// The source rectangle. - /// The transformation matrix. - /// The - protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) - { - return sourceDimensions; - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs deleted file mode 100644 index 8c851d7d8..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Runtime.CompilerServices; - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Provides methods that allow the resizing of images using various algorithms. - /// Adapted from - /// - /// The pixel format. - internal abstract class ResamplingWeightedProcessor : TransformProcessorBase - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The to use for buffer allocations. - /// The sampler to perform the resize operation. - /// The target width. - /// The target height. - /// - /// The structure that specifies the portion of the target image object to draw to. - /// - protected ResamplingWeightedProcessor(MemoryManager memoryManager, IResampler sampler, int width, int height, Rectangle resizeRectangle) - { - Guard.NotNull(memoryManager, nameof(memoryManager)); - Guard.NotNull(sampler, nameof(sampler)); - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); - - this.MemoryManager = memoryManager; - this.Sampler = sampler; - this.Width = width; - this.Height = height; - this.ResizeRectangle = resizeRectangle; - } - - /// - /// Gets the sampler to perform the resize operation. - /// - public IResampler Sampler { get; } - - /// - /// Gets or sets the width. - /// - public int Width { get; protected set; } - - /// - /// Gets or sets the height. - /// - public int Height { get; protected set; } - - /// - /// Gets or sets the resize rectangle. - /// - public Rectangle ResizeRectangle { get; protected set; } - - protected MemoryManager MemoryManager { get; } - - /// - /// Gets or sets the horizontal weights. - /// - protected WeightsBuffer HorizontalWeights { get; set; } - - /// - /// Gets or sets the vertical weights. - /// - protected WeightsBuffer VerticalWeights { get; set; } - - /// - /// Computes the weights to apply at each pixel when resizing. - /// - /// The destination size - /// The source size - /// The - // TODO: Made internal to simplify experimenting with weights data. Make it protected again when finished figuring out how to optimize all the stuff! - internal unsafe WeightsBuffer PrecomputeWeights(int destinationSize, int sourceSize) - { - float ratio = (float)sourceSize / destinationSize; - float scale = ratio; - - if (scale < 1F) - { - scale = 1F; - } - - IResampler sampler = this.Sampler; - float radius = MathF.Ceiling(scale * sampler.Radius); - var result = new WeightsBuffer(this.MemoryManager, sourceSize, destinationSize); - - for (int i = 0; i < destinationSize; i++) - { - float center = ((i + .5F) * ratio) - .5F; - - // Keep inside bounds. - int left = (int)Math.Ceiling(center - radius); - if (left < 0) - { - left = 0; - } - - int right = (int)Math.Floor(center + radius); - if (right > sourceSize - 1) - { - right = sourceSize - 1; - } - - float sum = 0; - - WeightsWindow ws = result.GetWeightsWindow(i, left, right); - result.Weights[i] = ws; - - ref float weightsBaseRef = ref ws.GetStartReference(); - - for (int j = left; j <= right; j++) - { - float weight = sampler.GetValue((j - center) / scale); - sum += weight; - - // weights[j - left] = weight: - Unsafe.Add(ref weightsBaseRef, j - left) = weight; - } - - // Normalise, best to do it here rather than in the pixel loop later on. - if (sum > 0) - { - for (int w = 0; w < ws.Length; w++) - { - // weights[w] = weights[w] / sum: - ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w); - wRef = wRef / sum; - } - } - } - - return result; - } - - /// - protected override void BeforeApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) - { - if (!(this.Sampler is NearestNeighborResampler)) - { - this.HorizontalWeights = this.PrecomputeWeights( - this.ResizeRectangle.Width, - sourceRectangle.Width); - - this.VerticalWeights = this.PrecomputeWeights( - this.ResizeRectangle.Height, - sourceRectangle.Height); - } - } - - /// - protected override void AfterApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) - { - base.AfterApply(source, destination, sourceRectangle, configuration); - this.HorizontalWeights?.Dispose(); - this.HorizontalWeights = null; - this.VerticalWeights?.Dispose(); - this.VerticalWeights = null; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index 2c18dc29b..2e73920ba 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -16,56 +16,224 @@ namespace SixLabors.ImageSharp.Processing.Processors { /// /// Provides methods that allow the resizing of images using various algorithms. + /// Adapted from /// /// The pixel format. - internal class ResizeProcessor : ResamplingWeightedProcessor + internal class ResizeProcessor : TransformProcessorBase where TPixel : struct, IPixel { + // The following fields are not immutable but are optionally created on demand. + private WeightsBuffer horizontalWeights; + private WeightsBuffer verticalWeights; + + /// + /// Initializes a new instance of the class. + /// + /// The resize options + /// The source image size + public ResizeProcessor(ResizeOptions options, Size sourceSize) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(options.Sampler, nameof(options.Sampler)); + + int tempWidth = options.Size.Width; + int tempHeight = options.Size.Height; + + // Ensure size is populated across both dimensions. + // These dimensions are used to calculate the final dimensions determined by the mode algorithm. + if (tempWidth == 0 && tempHeight > 0) + { + tempWidth = (int)MathF.Round(sourceSize.Width * tempHeight / (float)sourceSize.Height); + } + + if (tempHeight == 0 && tempWidth > 0) + { + tempHeight = (int)MathF.Round(sourceSize.Height * tempWidth / (float)sourceSize.Width); + } + + Guard.MustBeGreaterThan(tempWidth, 0, nameof(tempWidth)); + Guard.MustBeGreaterThan(tempHeight, 0, nameof(tempHeight)); + + (Size size, Rectangle rectangle) locationBounds = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options, tempWidth, tempHeight); + + this.Sampler = options.Sampler; + this.Width = locationBounds.size.Width; + this.Height = locationBounds.size.Height; + this.ResizeRectangle = locationBounds.rectangle; + this.Compand = options.Compand; + } + /// /// Initializes a new instance of the class. /// - /// The to use for buffer allocations. /// The sampler to perform the resize operation. /// The target width. /// The target height. - public ResizeProcessor(MemoryManager memoryManager, IResampler sampler, int width, int height) - : base(memoryManager, sampler, width, height, new Rectangle(0, 0, width, height)) + /// The source image size + public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize) + : this(sampler, width, height, sourceSize, new Rectangle(0, 0, width, height), false) { } /// /// Initializes a new instance of the class. /// - /// The to use for buffer allocations. /// The sampler to perform the resize operation. /// The target width. /// The target height. + /// The source image size /// /// The structure that specifies the portion of the target image object to draw to. /// - public ResizeProcessor(MemoryManager memoryManager, IResampler sampler, int width, int height, Rectangle resizeRectangle) - : base(memoryManager, sampler, width, height, resizeRectangle) + /// Whether to compress or expand individual pixel color values on processing. + public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle resizeRectangle, bool compand) { + Guard.NotNull(sampler, nameof(sampler)); + + // Ensure size is populated across both dimensions. + if (width == 0 && height > 0) + { + width = (int)MathF.Round(sourceSize.Width * height / (float)sourceSize.Height); + resizeRectangle.Width = width; + } + + if (height == 0 && width > 0) + { + height = (int)MathF.Round(sourceSize.Height * width / (float)sourceSize.Width); + resizeRectangle.Height = height; + } + + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.Sampler = sampler; + this.Width = width; + this.Height = height; + this.ResizeRectangle = resizeRectangle; + this.Compand = compand; } /// - /// Gets or sets a value indicating whether to compress or expand individual pixel color values on processing. + /// Gets the sampler to perform the resize operation. /// - public bool Compand { get; set; } + public IResampler Sampler { get; } + + /// + /// Gets the target width. + /// + public int Width { get; } + + /// + /// Gets the target height. + /// + public int Height { get; } + + /// + /// Gets the resize rectangle. + /// + public Rectangle ResizeRectangle { get; } + + /// + /// Gets a value indicating whether to compress or expand individual pixel color values on processing. + /// + public bool Compand { get; } + + /// + /// Computes the weights to apply at each pixel when resizing. + /// + /// The to use for buffer allocations + /// The destination size + /// The source size + /// The + // TODO: Made internal to simplify experimenting with weights data. Make it private when finished figuring out how to optimize all the stuff! + internal WeightsBuffer PrecomputeWeights(MemoryManager memoryManager, int destinationSize, int sourceSize) + { + float ratio = (float)sourceSize / destinationSize; + float scale = ratio; + + if (scale < 1F) + { + scale = 1F; + } + + IResampler sampler = this.Sampler; + float radius = MathF.Ceiling(scale * sampler.Radius); + var result = new WeightsBuffer(memoryManager, sourceSize, destinationSize); + + for (int i = 0; i < destinationSize; i++) + { + float center = ((i + .5F) * ratio) - .5F; + + // Keep inside bounds. + int left = (int)Math.Ceiling(center - radius); + if (left < 0) + { + left = 0; + } + + int right = (int)Math.Floor(center + radius); + if (right > sourceSize - 1) + { + right = sourceSize - 1; + } + + float sum = 0; + + WeightsWindow ws = result.GetWeightsWindow(i, left, right); + result.Weights[i] = ws; + + ref float weightsBaseRef = ref ws.GetStartReference(); + + for (int j = left; j <= right; j++) + { + float weight = sampler.GetValue((j - center) / scale); + sum += weight; + + // weights[j - left] = weight: + Unsafe.Add(ref weightsBaseRef, j - left) = weight; + } + + // Normalize, best to do it here rather than in the pixel loop later on. + if (sum > 0) + { + for (int w = 0; w < ws.Length; w++) + { + // weights[w] = weights[w] / sum: + ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w); + wRef = wRef / sum; + } + } + } + + return result; + } /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - Configuration config = source.GetConfiguration(); + // We will always be creating the clone even for mutate because we may need to resize the canvas + IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetMemoryManager(), this.Width, this.Height, x.MetaData.Clone())); // this will create places holders - // We will always be creating the clone even for mutate because thats the way this base processor works - // ------------ - // For resize we know we are going to populate every pixel with fresh data and we want a different target size so - // let's manually clone an empty set of images at the correct target and then have the base class process them in turn. - IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetConfiguration().MemoryManager, this.Width, this.Height, x.MetaData.Clone())); // this will create places holders - var image = new Image(config, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + } - return image; + /// + protected override void BeforeApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + if (!(this.Sampler is NearestNeighborResampler)) + { + // TODO: Optimization opportunity: if we could assume that all frames are of the same size, we can move this into 'BeforeImageApply()` + this.horizontalWeights = this.PrecomputeWeights( + source.MemoryManager, + this.ResizeRectangle.Width, + sourceRectangle.Width); + + this.verticalWeights = this.PrecomputeWeights( + source.MemoryManager, + this.ResizeRectangle.Height, + sourceRectangle.Height); + } } /// @@ -124,7 +292,7 @@ namespace SixLabors.ImageSharp.Processing.Processors // First process the columns. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. // TODO: Using a transposed variant of 'firstPassPixels' could eliminate the need for the WeightsWindow.ComputeWeightedColumnSum() method, and improve speed! - using (Buffer2D firstPassPixels = this.MemoryManager.Allocate2D(width, source.Height)) + using (Buffer2D firstPassPixels = source.MemoryManager.Allocate2D(width, source.Height)) { firstPassPixels.Buffer.Clear(); @@ -145,7 +313,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { for (int x = minX; x < maxX; x++) { - WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; + WeightsWindow window = this.horizontalWeights.Weights[x - startX]; firstPassRow[x] = window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX); } } @@ -153,7 +321,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { for (int x = minX; x < maxX; x++) { - WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; + WeightsWindow window = this.horizontalWeights.Weights[x - startX]; firstPassRow[x] = window.ComputeWeightedRowSum(tempRowSpan, sourceX); } } @@ -166,8 +334,8 @@ namespace SixLabors.ImageSharp.Processing.Processors configuration.ParallelOptions, y => { - // Ensure offsets are normalised for cropping and padding. - WeightsWindow window = this.VerticalWeights.Weights[y - startY]; + // Ensure offsets are normalized for cropping and padding. + WeightsWindow window = this.verticalWeights.Weights[y - startY]; Span targetRow = cloned.GetPixelRowSpan(y); if (this.Compand) @@ -196,5 +364,17 @@ namespace SixLabors.ImageSharp.Processing.Processors }); } } + + /// + protected override void AfterApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + base.AfterApply(source, destination, sourceRectangle, configuration); + + // TODO: An exception in the processing chain can leave these buffers undisposed. We should consider making image processors IDisposable! + this.horizontalWeights?.Dispose(); + this.horizontalWeights = null; + this.verticalWeights?.Dispose(); + this.verticalWeights = null; + } } } \ 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 b93c86915..824ae4310 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -22,8 +22,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// /// The angle of rotation in degrees. - public RotateProcessor(float degrees) - : this(degrees, KnownResamplers.Bicubic) + /// The source image size + public RotateProcessor(float degrees, Size sourceSize) + : this(degrees, KnownResamplers.Bicubic, sourceSize) { } @@ -32,8 +33,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The angle of rotation in degrees. /// The sampler to perform the rotating operation. - public RotateProcessor(float degrees, IResampler sampler) - : base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler) + /// The source image size + public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) + : base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler, sourceSize) { this.Degrees = degrees; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 8c47b3527..8e3ab7c34 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -18,8 +18,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The angle in degrees to perform the skew along the x-axis. /// The angle in degrees to perform the skew along the y-axis. - public SkewProcessor(float degreesX, float degreesY) - : this(degreesX, degreesY, KnownResamplers.Bicubic) + /// The source image size + public SkewProcessor(float degreesX, float degreesY, Size sourceSize) + : this(degreesX, degreesY, KnownResamplers.Bicubic, sourceSize) { } @@ -29,8 +30,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The angle in degrees to perform the skew along the x-axis. /// The angle in degrees to perform the skew along the y-axis. /// The sampler to perform the skew operation. - public SkewProcessor(float degreesX, float degreesY, IResampler sampler) - : base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler) + /// The source image size + public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) + : base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler, sourceSize) { this.DegreesX = degreesX; this.DegreesY = degreesY; diff --git a/src/ImageSharp/Processing/Transforms/Options/ResizeHelper.cs b/src/ImageSharp/Processing/Transforms/Options/ResizeHelper.cs index 17a0cc428..ba6f4509d 100644 --- a/src/ImageSharp/Processing/Transforms/Options/ResizeHelper.cs +++ b/src/ImageSharp/Processing/Transforms/Options/ResizeHelper.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing @@ -17,52 +16,39 @@ namespace SixLabors.ImageSharp.Processing /// /// Calculates the target location and bounds to perform the resize operation against. /// - /// The pixel format. - /// The source image. + /// The source image size. /// The resize options. + /// The target width + /// The target height /// - /// The . + /// The . /// - public static Rectangle CalculateTargetLocationAndBounds(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + public static (Size, Rectangle) CalculateTargetLocationAndBounds(Size sourceSize, ResizeOptions options, int width, int height) { switch (options.Mode) { case ResizeMode.Crop: - return CalculateCropRectangle(source, options); + return CalculateCropRectangle(sourceSize, options, width, height); case ResizeMode.Pad: - return CalculatePadRectangle(source, options); + return CalculatePadRectangle(sourceSize, options, width, height); case ResizeMode.BoxPad: - return CalculateBoxPadRectangle(source, options); + return CalculateBoxPadRectangle(sourceSize, options, width, height); case ResizeMode.Max: - return CalculateMaxRectangle(source, options); + return CalculateMaxRectangle(sourceSize, options, width, height); case ResizeMode.Min: - return CalculateMinRectangle(source, options); + return CalculateMinRectangle(sourceSize, options, width, height); // Last case ResizeMode.Stretch: default: - return new Rectangle(0, 0, options.Size.Width, options.Size.Height); + return (new Size(width, height), new Rectangle(0, 0, width, height)); } } - /// - /// Calculates the target rectangle for crop mode. - /// - /// The pixel format. - /// The source image. - /// The resize options. - /// - /// The . - /// - private static Rectangle CalculateCropRectangle(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + private static (Size, Rectangle) CalculateCropRectangle(Size source, ResizeOptions options, int width, int height) { - int width = options.Size.Width; - int height = options.Size.Height; - if (width <= 0 || height <= 0) { - return new Rectangle(0, 0, source.Width, source.Height); + return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); } float ratio; @@ -161,27 +147,14 @@ namespace SixLabors.ImageSharp.Processing destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight); } - return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); + return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); } - /// - /// Calculates the target rectangle for pad mode. - /// - /// The pixel format. - /// The source image. - /// The resize options. - /// - /// The . - /// - private static Rectangle CalculatePadRectangle(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + private static (Size, Rectangle) CalculatePadRectangle(Size source, ResizeOptions options, int width, int height) { - int width = options.Size.Width; - int height = options.Size.Height; - if (width <= 0 || height <= 0) { - return new Rectangle(0, 0, source.Width, source.Height); + return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); } float ratio; @@ -242,27 +215,14 @@ namespace SixLabors.ImageSharp.Processing } } - return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); + return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); } - /// - /// Calculates the target rectangle for box pad mode. - /// - /// The pixel format. - /// The source image. - /// The resize options. - /// - /// The . - /// - private static Rectangle CalculateBoxPadRectangle(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + private static (Size, Rectangle) CalculateBoxPadRectangle(Size source, ResizeOptions options, int width, int height) { - int width = options.Size.Width; - int height = options.Size.Height; - if (width <= 0 || height <= 0) { - return new Rectangle(0, 0, source.Width, source.Height); + return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); } int sourceWidth = source.Width; @@ -325,27 +285,15 @@ namespace SixLabors.ImageSharp.Processing break; } - return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); + return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); } // Switch to pad mode to downscale and calculate from there. - return CalculatePadRectangle(source, options); + return CalculatePadRectangle(source, options, width, height); } - /// - /// Calculates the target rectangle for max mode. - /// - /// The pixel format. - /// The source image. - /// The resize options. - /// - /// The . - /// - private static Rectangle CalculateMaxRectangle(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + private static (Size, Rectangle) CalculateMaxRectangle(Size source, ResizeOptions options, int width, int height) { - int width = options.Size.Width; - int height = options.Size.Height; int destinationWidth = width; int destinationHeight = height; @@ -369,24 +317,11 @@ namespace SixLabors.ImageSharp.Processing } // Replace the size to match the rectangle. - options.Size = new Size(width, height); - return new Rectangle(0, 0, destinationWidth, destinationHeight); + return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); } - /// - /// Calculates the target rectangle for min mode. - /// - /// The pixel format. - /// The source image. - /// The resize options. - /// - /// The . - /// - private static Rectangle CalculateMinRectangle(ImageFrame source, ResizeOptions options) - where TPixel : struct, IPixel + private static (Size, Rectangle) CalculateMinRectangle(Size source, ResizeOptions options, int width, int height) { - int width = options.Size.Width; - int height = options.Size.Height; int sourceWidth = source.Width; int sourceHeight = source.Height; int destinationWidth; @@ -395,8 +330,7 @@ namespace SixLabors.ImageSharp.Processing // Don't upscale if (width > sourceWidth || height > sourceHeight) { - options.Size = new Size(sourceWidth, sourceHeight); - return new Rectangle(0, 0, sourceWidth, sourceHeight); + return (new Size(sourceWidth, sourceWidth), new Rectangle(0, 0, sourceWidth, sourceHeight)); } // Fractional variants for preserving aspect ratio. @@ -438,8 +372,7 @@ namespace SixLabors.ImageSharp.Processing } // Replace the size to match the rectangle. - options.Size = new Size(width, height); - return new Rectangle(0, 0, destinationWidth, destinationHeight); + return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs b/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs index d20eaefb1..f13fa77c9 100644 --- a/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs +++ b/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs @@ -41,6 +41,6 @@ namespace SixLabors.ImageSharp.Processing /// Gets or sets a value indicating whether to compress /// or expand individual pixel colors the value on processing. /// - public bool Compand { get; set; } + public bool Compand { get; set; } = false; } } diff --git a/src/ImageSharp/Processing/Transforms/Pad.cs b/src/ImageSharp/Processing/Transforms/Pad.cs index eb0f2e9c2..2f637aa16 100644 --- a/src/ImageSharp/Processing/Transforms/Pad.cs +++ b/src/ImageSharp/Processing/Transforms/Pad.cs @@ -1,10 +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; using SixLabors.Primitives; namespace SixLabors.ImageSharp @@ -25,11 +23,11 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Pad(this IImageProcessingContext source, int width, int height) where TPixel : struct, IPixel { - ResizeOptions options = new ResizeOptions + var options = new ResizeOptions { Size = new Size(width, height), Mode = ResizeMode.BoxPad, - Sampler = new NearestNeighborResampler() + Sampler = KnownResamplers.NearestNeighbor }; return Resize(source, options); diff --git a/src/ImageSharp/Processing/Transforms/Resize.cs b/src/ImageSharp/Processing/Transforms/Resize.cs index 18def03e6..285f3206d 100644 --- a/src/ImageSharp/Processing/Transforms/Resize.cs +++ b/src/ImageSharp/Processing/Transforms/Resize.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; @@ -24,29 +23,10 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, ResizeOptions options) where TPixel : struct, IPixel - { - return source.Apply(img => - { - // Cheat and bound through a run, inside here we should just be mutating, this really needs moving over to a processor - // Ensure size is populated across both dimensions. - if (options.Size.Width == 0 && options.Size.Height > 0) - { - options.Size = new Size((int)MathF.Round(img.Width * options.Size.Height / (float)img.Height), options.Size.Height); - } - - if (options.Size.Height == 0 && options.Size.Width > 0) - { - options.Size = new Size(options.Size.Width, (int)MathF.Round(img.Height * options.Size.Width / (float)img.Width)); - } - - Rectangle targetRectangle = ResizeHelper.CalculateTargetLocationAndBounds(img.Frames.RootFrame, options); - - img.Mutate(x => Resize(x, options.Size.Width, options.Size.Height, options.Sampler, targetRectangle, options.Compand)); - }); - } + => source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize())); /// - /// Resizes an image to the given . + /// Resizes an image to the given . /// /// The pixel format. /// The image to resize. @@ -55,12 +35,10 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size) where TPixel : struct, IPixel - { - return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, false); - } + => Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, false); /// - /// Resizes an image to the given . + /// Resizes an image to the given . /// /// The pixel format. /// The image to resize. @@ -70,9 +48,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size, bool compand) where TPixel : struct, IPixel - { - return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, compand); - } + => Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, compand); /// /// Resizes an image to the given width and height. @@ -85,9 +61,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height) where TPixel : struct, IPixel - { - return Resize(source, width, height, KnownResamplers.Bicubic, false); - } + => Resize(source, width, height, KnownResamplers.Bicubic, false); /// /// Resizes an image to the given width and height. @@ -101,9 +75,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, bool compand) where TPixel : struct, IPixel - { - return Resize(source, width, height, KnownResamplers.Bicubic, compand); - } + => Resize(source, width, height, KnownResamplers.Bicubic, compand); /// /// Resizes an image to the given width and height with the given sampler. @@ -117,9 +89,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler) where TPixel : struct, IPixel - { - return Resize(source, width, height, sampler, false); - } + => Resize(source, width, height, sampler, false); /// /// Resizes an image to the given width and height with the given sampler. @@ -133,9 +103,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size, IResampler sampler, bool compand) where TPixel : struct, IPixel - { - return Resize(source, size.Width, size.Height, sampler, new Rectangle(0, 0, size.Width, size.Height), compand); - } + => Resize(source, size.Width, size.Height, sampler, new Rectangle(0, 0, size.Width, size.Height), compand); /// /// Resizes an image to the given width and height with the given sampler. @@ -150,9 +118,7 @@ namespace SixLabors.ImageSharp /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler, bool compand) where TPixel : struct, IPixel - { - return Resize(source, width, height, sampler, new Rectangle(0, 0, width, height), compand); - } + => Resize(source, width, height, sampler, new Rectangle(0, 0, width, height), compand); /// /// Resizes an image to the given width and height with the given sampler and @@ -172,34 +138,19 @@ namespace SixLabors.ImageSharp /// Whether to compress and expand the image color-space to gamma correct the image during processing. /// The /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image - public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler, Rectangle sourceRectangle, Rectangle targetRectangle, bool compand) + public static IImageProcessingContext Resize( + this IImageProcessingContext source, + int width, + int height, + IResampler sampler, + Rectangle sourceRectangle, + Rectangle targetRectangle, + bool compand) where TPixel : struct, IPixel - { - return source.Apply(img => - { - // TODO : Stop cheating here and move this stuff into the processors itself - if (width == 0 && height > 0) - { - width = (int)MathF.Round(img.Width * height / (float)img.Height); - targetRectangle.Width = width; - } - - if (height == 0 && width > 0) - { - height = (int)MathF.Round(img.Height * width / (float)img.Width); - targetRectangle.Height = height; - } - - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); - - img.Mutate(x => x.ApplyProcessor(new ResizeProcessor(source.MemoryManager, sampler, width, height, targetRectangle) { Compand = compand }, sourceRectangle)); - }); - } + => source.ApplyProcessor(new ResizeProcessor(sampler, width, height, source.GetCurrentSize(), targetRectangle, compand), sourceRectangle); /// - /// Resizes an image to the given width and height with the given sampler and - /// source rectangle. + /// Resizes an image to the given width and height with the given sampler and source rectangle. /// /// The pixel format. /// The image to resize. @@ -212,29 +163,14 @@ namespace SixLabors.ImageSharp /// Whether to compress and expand the image color-space to gamma correct the image during processing. /// The /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image - public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, IResampler sampler, Rectangle targetRectangle, bool compand) + public static IImageProcessingContext Resize( + this IImageProcessingContext source, + int width, + int height, + IResampler sampler, + Rectangle targetRectangle, + bool compand) where TPixel : struct, IPixel - { - return source.Apply(img => - { - // TODO : stop cheating here and move this stuff into the processors itself - if (width == 0 && height > 0) - { - width = (int)MathF.Round(img.Width * height / (float)img.Height); - targetRectangle.Width = width; - } - - if (height == 0 && width > 0) - { - height = (int)MathF.Round(img.Height * width / (float)img.Width); - targetRectangle.Height = height; - } - - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); - - img.Mutate(x => x.ApplyProcessor(new ResizeProcessor(source.MemoryManager, sampler, width, height, targetRectangle) { Compand = compand })); - }); - } + => source.ApplyProcessor(new ResizeProcessor(sampler, width, height, source.GetCurrentSize(), targetRectangle, compand)); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index 69fb7ebf0..faecdf91f 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -44,6 +44,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new RotateProcessor(degrees, sampler)); + => source.ApplyProcessor(new RotateProcessor(degrees, sampler, source.GetCurrentSize())); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index 0613a690b..fb054211a 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -35,6 +35,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new SkewProcessor(degreesX, degreesY, sampler)); + => source.ApplyProcessor(new SkewProcessor(degreesX, degreesY, sampler, source.GetCurrentSize())); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index 326ed7586..66d5d5de7 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) where TPixel : struct, IPixel - => Transform(source, matrix, sampler, Size.Empty); + => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, source.GetCurrentSize())); /// /// Transforms an image by the given matrix using the specified sampling algorithm @@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp /// The internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) where TPixel : struct, IPixel - => Transform(source, matrix, sampler, Rectangle.Empty); + => source.ApplyProcessor(new ProjectiveTransformProcessor(matrix, sampler, source.GetCurrentSize())); /// /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. diff --git a/tests/ImageSharp.Tests/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Tests/BaseImageOperationsExtensionTest.cs index 37696987c..ec46e6610 100644 --- a/tests/ImageSharp.Tests/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Tests/BaseImageOperationsExtensionTest.cs @@ -16,29 +16,31 @@ namespace SixLabors.ImageSharp.Tests private readonly FakeImageOperationsProvider.FakeImageOperations internalOperations; protected readonly Rectangle rect; protected readonly GraphicsOptions options; + private Image source; public BaseImageOperationsExtensionTest() { - this.options = new GraphicsOptions(false) { }; + this.options = new GraphicsOptions(false); + this.source = new Image(91 + 324, 123 + 56); this.rect = new Rectangle(91, 123, 324, 56); // make this random? - this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(null, false); + this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source, false); this.operations = this.internalOperations; } public T Verify(int index = 0) { - Assert.InRange(index, 0, this.internalOperations.applied.Count - 1); + Assert.InRange(index, 0, this.internalOperations.Applied.Count - 1); - var operation = this.internalOperations.applied[index]; + var operation = this.internalOperations.Applied[index]; return Assert.IsType(operation.Processor); } public T Verify(Rectangle rect, int index = 0) { - Assert.InRange(index, 0, this.internalOperations.applied.Count - 1); + Assert.InRange(index, 0, this.internalOperations.Applied.Count - 1); - var operation = this.internalOperations.applied[index]; + var operation = this.internalOperations.Applied[index]; Assert.Equal(rect, operation.Rectangle); return Assert.IsType(operation.Processor); diff --git a/tests/ImageSharp.Tests/FakeImageOperationsProvider.cs b/tests/ImageSharp.Tests/FakeImageOperationsProvider.cs index c2ed7238d..db1e7903d 100644 --- a/tests/ImageSharp.Tests/FakeImageOperationsProvider.cs +++ b/tests/ImageSharp.Tests/FakeImageOperationsProvider.cs @@ -1,15 +1,12 @@ // 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.Text; - +using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Tests @@ -26,13 +23,13 @@ namespace SixLabors.ImageSharp.Tests public IEnumerable> Created(Image source) where TPixel : struct, IPixel { return this.ImageOperators.OfType>() - .Where(x => x.source == source); + .Where(x => x.Source == source); } - public IEnumerable.AppliedOpperation> AppliedOperations(Image source) where TPixel : struct, IPixel + public IEnumerable.AppliedOperation> AppliedOperations(Image source) where TPixel : struct, IPixel { return Created(source) - .SelectMany(x => x.applied); + .SelectMany(x => x.Applied); } public IInternalImageProcessingContext CreateImageProcessingContext(Image source, bool mutate) where TPixel : struct, IPixel @@ -42,36 +39,36 @@ namespace SixLabors.ImageSharp.Tests return op; } - public class FakeImageOperations : IInternalImageProcessingContext where TPixel : struct, IPixel { - public Image source; - - public List applied = new List(); - public bool mutate; + private bool mutate; public FakeImageOperations(Image source, bool mutate) { this.mutate = mutate; - if (mutate) - { - this.source = source; - } - else - { - this.source = source?.Clone(); - } + this.Source = mutate ? source : source?.Clone(); } + public Image Source { get; } + + public List Applied { get; } = new List(); + + public MemoryManager MemoryManager => this.Source.GetConfiguration().MemoryManager; + public Image Apply() { - return source; + return this.Source; + } + + public Size GetCurrentSize() + { + return this.Source.Size(); } public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle) { - applied.Add(new AppliedOpperation + this.Applied.Add(new AppliedOperation { Processor = processor, Rectangle = rectangle @@ -81,16 +78,14 @@ namespace SixLabors.ImageSharp.Tests public IImageProcessingContext ApplyProcessor(IImageProcessor processor) { - applied.Add(new AppliedOpperation + this.Applied.Add(new AppliedOperation { Processor = processor }); return this; } - public MemoryManager MemoryManager => this.source.GetConfiguration().MemoryManager; - - public struct AppliedOpperation + public struct AppliedOperation { public Rectangle? Rectangle { get; set; } public IImageProcessor Processor { get; set; } diff --git a/tests/ImageSharp.Tests/Image/ImageProcessingContextTests.cs b/tests/ImageSharp.Tests/Image/ImageProcessingContextTests.cs new file mode 100644 index 000000000..f8f7b6758 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageProcessingContextTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class ImageProcessingContextTests + { + [Fact] + public void MutatedSizeIsAccuratePerOperation() + { + var x500 = new Size(500, 500); + var x400 = new Size(400, 400); + var x300 = new Size(300, 300); + var x200 = new Size(200, 200); + var x100 = new Size(100, 100); + using (var image = new Image(500, 500)) + { + image.Mutate(x => + x.AssertSize(x500) + .Resize(x400).AssertSize(x400) + .Resize(x300).AssertSize(x300) + .Resize(x200).AssertSize(x200) + .Resize(x100).AssertSize(x100)); + } + } + + [Fact] + public void ClonedSizeIsAccuratePerOperation() + { + var x500 = new Size(500, 500); + var x400 = new Size(400, 400); + var x300 = new Size(300, 300); + var x200 = new Size(200, 200); + var x100 = new Size(100, 100); + using (var image = new Image(500, 500)) + { + image.Clone(x => + x.AssertSize(x500) + .Resize(x400).AssertSize(x400) + .Resize(x300).AssertSize(x300) + .Resize(x200).AssertSize(x200) + .Resize(x100).AssertSize(x100)); + } + } + } + + public static class SizeAssertationExtensions + { + public static IImageProcessingContext AssertSize(this IImageProcessingContext context, Size size) + { + Assert.Equal(size, context.GetCurrentSize()); + return context; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ImageOperationTests.cs b/tests/ImageSharp.Tests/ImageOperationTests.cs index 59722a84d..a5d6d2eb9 100644 --- a/tests/ImageSharp.Tests/ImageOperationTests.cs +++ b/tests/ImageSharp.Tests/ImageOperationTests.cs @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests { var operations = new FakeImageOperationsProvider.FakeImageOperations(null, false); operations.ApplyProcessors(this.processor); - Assert.Contains(this.processor, operations.applied.Select(x => x.Processor)); + Assert.Contains(this.processor, operations.Applied.Select(x => x.Processor)); } public void Dispose() diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs index 54a3b7418..2e1b43c38 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs @@ -6,13 +6,12 @@ using System.IO; using System.Text; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; -using Xunit; +using SixLabors.Primitives; + using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - using SixLabors.ImageSharp.Memory; - public class ResizeProfilingBenchmarks : MeasureFixture { public ResizeProfilingBenchmarks(ITestOutputHelper output) @@ -40,9 +39,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // [Fact] public void PrintWeightsData() { - var proc = new ResizeProcessor(Configuration.Default.MemoryManager, new BicubicResampler(), 200, 200); + var size = new Size(500, 500); + var proc = new ResizeProcessor(KnownResamplers.Bicubic, 200, 200, size); - WeightsBuffer weights = proc.PrecomputeWeights(200, 500); + WeightsBuffer weights = proc.PrecomputeWeights(Configuration.Default.MemoryManager, proc.Width, size.Width); var bld = new StringBuilder(); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs index 58fc7fa80..a9c2922d4 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs @@ -1,20 +1,30 @@ // 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; + using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { + + public class PadTest : BaseImageOperationsExtensionTest { -#pragma warning disable xUnit1004 // Test methods should not be skipped - [Fact(Skip = "Skip this is a helper around resize, skip until resize can be refactord")] -#pragma warning restore xUnit1004 // Test methods should not be skipped - public void Pad_width_height_ResizeProcessorWithCorrectOPtionsSet() + [Fact] + public void PadWidthHeightResizeProcessorWithCorrectOptionsSet() { - throw new NotImplementedException("Write test here"); + int width = 500; + int height = 565; + IResampler sampler = KnownResamplers.NearestNeighbor; + + this.operations.Pad(width, height); + ResizeProcessor resizeProcessor = this.Verify>(); + + Assert.Equal(width, resizeProcessor.Width); + Assert.Equal(height, resizeProcessor.Height); + Assert.Equal(sampler, resizeProcessor.Sampler); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs index d2f5cb2c3..b51d342cf 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs @@ -1,23 +1,91 @@ // 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.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { + using SixLabors.ImageSharp.Processing.Processors; + public class ResizeTests : BaseImageOperationsExtensionTest { -#pragma warning disable xUnit1004 // Test methods should not be skipped - [Fact(Skip = "Skip resize tests as they need refactoring to be simpler and just pass data into the resize processor.")] -#pragma warning restore xUnit1004 // Test methods should not be skipped - public void TestMissing() + [Fact] + public void ResizeWidthAndHeight() + { + int width = 50; + int height = 100; + this.operations.Resize(width, height); + ResizeProcessor resizeProcessor = this.Verify>(); + + Assert.Equal(width, resizeProcessor.Width); + Assert.Equal(height, resizeProcessor.Height); + } + + [Fact] + public void ResizeWidthAndHeightAndSampler() + { + int width = 50; + int height = 100; + IResampler sampler = KnownResamplers.Lanczos3; + this.operations.Resize(width, height, sampler); + ResizeProcessor resizeProcessor = this.Verify>(); + + Assert.Equal(width, resizeProcessor.Width); + Assert.Equal(height, resizeProcessor.Height); + Assert.Equal(sampler, resizeProcessor.Sampler); + } + + [Fact] + public void ResizeWidthAndHeightAndSamplerAndCompand() { - // - throw new NotImplementedException("Write test here"); + int width = 50; + int height = 100; + IResampler sampler = KnownResamplers.Lanczos3; + bool compand = true; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + this.operations.Resize(width, height, sampler, compand); + ResizeProcessor resizeProcessor = this.Verify>(); + + Assert.Equal(width, resizeProcessor.Width); + Assert.Equal(height, resizeProcessor.Height); + Assert.Equal(sampler, resizeProcessor.Sampler); + Assert.Equal(compand, resizeProcessor.Compand); + } + + [Fact] + public void ResizeWithOptions() + { + int width = 50; + int height = 100; + IResampler sampler = KnownResamplers.Lanczos3; + bool compand = true; + ResizeMode mode = ResizeMode.Stretch; + + var resizeOptions = new ResizeOptions + { + Size = new Size(width, height), + Sampler = sampler, + Compand = compand, + Mode = mode + }; + + this.operations.Resize(resizeOptions); + ResizeProcessor resizeProcessor = this.Verify>(); + + Assert.Equal(width, resizeProcessor.Width); + Assert.Equal(height, resizeProcessor.Height); + Assert.Equal(sampler, resizeProcessor.Sampler); + Assert.Equal(compand, resizeProcessor.Compand); + + // Ensure options are not altered. + Assert.Equal(width, resizeOptions.Size.Width); + Assert.Equal(height, resizeOptions.Size.Height); + Assert.Equal(sampler, resizeOptions.Sampler); + Assert.Equal(compand, resizeOptions.Compand); + Assert.Equal(mode, resizeOptions.Mode); } } } \ No newline at end of file