diff --git a/README.md b/README.md index 130b3d6f0a..1432317c13 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor - [x] Size - [x] Point - [x] Ellipse -- Resampling algorithms. (Optional gamma correction, Performance improvements?) +- Resampling algorithms. (Optional gamma correction, resize modes, Performance improvements?) - [x] Box - [x] Bicubic - [x] Lanczos3 @@ -86,13 +86,18 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor - [x] Spline - [x] Triangle - [x] Welch +- Padding + - [x] Pad + - [x] ResizeMode.Pad + - [x] ResizeMode.BoxPad - Cropping - [x] Rectangular Crop - [ ] Elliptical Crop - [x] Entropy Crop + - [x] ResizeMode.Crop - Rotation/Skew - [x] Flip (90, 270, FlipType etc) - - [x] Rotate by angle and center point. + - [x] Rotate by angle and center point (Expandable canvas). - [x] Skew by x/y angles and center point. - ColorMatrix operations (Uses Matrix4x4) - [x] BlackWhite diff --git a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs index 71c40f6c05..f287232760 100644 --- a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs +++ b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs @@ -42,7 +42,7 @@ namespace ImageProcessorCore.Samplers { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - + if (sourceRectangle.Width < width || sourceRectangle.Height < height) { // If the source rectangle is smaller than the target perform a @@ -85,6 +85,26 @@ namespace ImageProcessorCore.Samplers } } + /// + /// Evenly pads an image to fit the new dimensions. + /// + /// The source image to pad. + /// The new width. + /// The new height. + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image Pad(this Image source, int width, int height, ProgressEventHandler progressHandler = null) + { + ResizeOptions options = new ResizeOptions + { + Size = new Size(width, height), + Mode = ResizeMode.BoxPad, + Sampler = new NearestNeighborResampler() + }; + + return Resize(source, options, progressHandler); + } + /// /// Resizes an image in accordance with the given . /// @@ -95,7 +115,7 @@ namespace ImageProcessorCore.Samplers /// Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image public static Image Resize(this Image source, ResizeOptions options, ProgressEventHandler progressHandler = null) { - // Ensure size is populated acros both dimensions. + // Ensure size is populated across both dimensions. if (options.Size.Width == 0 && options.Size.Height > 0) { options.Size = new Size(source.Width * options.Size.Height / source.Height, options.Size.Height); @@ -203,7 +223,7 @@ namespace ImageProcessorCore.Samplers } /// - /// Rotates an image by the given angle in degrees. + /// Rotates an image by the given angle in degrees, expanding the image to fit the rotated result. /// /// The image to rotate. /// The angle in degrees to perform the rotation. @@ -211,7 +231,7 @@ namespace ImageProcessorCore.Samplers /// The public static Image Rotate(this Image source, float degrees, ProgressEventHandler progressHandler = null) { - return Rotate(source, degrees, Rectangle.Center(source.Bounds), progressHandler); + return Rotate(source, degrees, Point.Empty, true, progressHandler); } /// @@ -220,11 +240,12 @@ namespace ImageProcessorCore.Samplers /// The image to rotate. /// The angle in degrees to perform the rotation. /// The center point at which to skew the image. + /// Whether to expand the image to fit the rotated result. /// A delegate which is called as progress is made processing the image. /// The - public static Image Rotate(this Image source, float degrees, Point center, ProgressEventHandler progressHandler = null) + public static Image Rotate(this Image source, float degrees, Point center, bool expand, ProgressEventHandler progressHandler = null) { - Rotate processor = new Rotate { Angle = degrees, Center = center }; + Rotate processor = new Rotate { Angle = degrees, Center = center, Expand = expand }; processor.OnProgress += progressHandler; try diff --git a/src/ImageProcessorCore/Samplers/Rotate.cs b/src/ImageProcessorCore/Samplers/Rotate.cs index bc10e2fdb0..1de1b57af7 100644 --- a/src/ImageProcessorCore/Samplers/Rotate.cs +++ b/src/ImageProcessorCore/Samplers/Rotate.cs @@ -3,10 +3,9 @@ // Licensed under the Apache License, Version 2.0. // -using System.Numerics; - namespace ImageProcessorCore.Samplers { + using System.Numerics; using System.Threading.Tasks; /// @@ -15,12 +14,20 @@ namespace ImageProcessorCore.Samplers public class Rotate : ImageSampler { /// - /// The angle of rotation. + /// The image used for storing the first pass pixels. + /// + private Image firstPass; + + /// + /// The angle of rotation in degrees. /// private float angle; + /// + public override int Parallelism { get; set; } = 1; + /// - /// Gets or sets the angle of rotation. + /// Gets or sets the angle of rotation in degrees. /// public float Angle { @@ -50,47 +57,84 @@ namespace ImageProcessorCore.Samplers /// public Point Center { get; set; } + /// + /// Gets or sets a value indicating whether to expand the canvas to fit the rotated image. + /// + public bool Expand { get; set; } + /// - protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) { - int targetY = targetRectangle.Y; - int targetBottom = targetRectangle.Bottom; - int startX = targetRectangle.X; - int endX = targetRectangle.Right; - float negativeAngle = -this.angle; - Point centre = this.Center == Point.Empty ? Rectangle.Center(sourceRectangle) : this.Center; + // If we are expanding we need to pad the bounds of the source rectangle. + // We can use the resizer in nearest neighbor mode to do this fairly quickly. + if (this.Expand) + { + // First find out how the target rectangle should be. + Rectangle rectangle = ImageMaths.GetBoundingRotatedRectangle(source.Width, source.Height, -this.angle); + + ResizeOptions options = new ResizeOptions + { + Size = new Size(rectangle.Width, rectangle.Height), + Mode = ResizeMode.BoxPad, + Sampler = new NearestNeighborResampler() + }; - // Scaling factors - float widthFactor = source.Width / (float)target.Width; - float heightFactor = source.Height / (float)target.Height; + // Get the padded bounds and resize the image. + Rectangle bounds = ResizeHelper.CalculateTargetLocationAndBounds(source, options); + this.firstPass = new Image(rectangle.Width, rectangle.Height); + target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]); + new Resize(new NearestNeighborResampler()).Apply(this.firstPass, source, rectangle.Width, rectangle.Height, bounds, sourceRectangle); + } + else + { + // Just clone the pixels across. + this.firstPass = new Image(source.Width, source.Height); + this.firstPass.ClonePixels(source.Width, source.Height, source.Pixels); + } + } - Matrix3x2 rotation = Point.CreateRotatation( centre, negativeAngle ); + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + { + int targetY = this.firstPass.Bounds.Y; + int targetHeight = this.firstPass.Height; + int startX = this.firstPass.Bounds.X; + int endX = this.firstPass.Bounds.Right; + float negativeAngle = -this.angle; + Point centre = this.Center == Point.Empty ? Rectangle.Center(this.firstPass.Bounds) : this.Center; + Matrix3x2 rotation = Point.CreateRotatation(centre, negativeAngle); + // Since we are not working in parallel we use full height and width of the first pass image. Parallel.For( - startY, - endY, + 0, + targetHeight, y => { - if (y >= targetY && y < targetBottom) + // Y coordinates of source points + int originY = y - targetY; + + for (int x = startX; x < endX; x++) { - // Y coordinates of source points - int originY = (int)((y - targetY) * heightFactor); + // X coordinates of source points + int originX = x - startX; - for (int x = startX; x < endX; x++) + // Rotate at the centre point + Point rotated = Point.Rotate(new Point(originX, originY), rotation); + if (this.firstPass.Bounds.Contains(rotated.X, rotated.Y)) { - // X coordinates of source points - int originX = (int)((x - startX) * widthFactor); - - // Rotate at the centre point - Point rotated = Point.Rotate(new Point(originX, originY), rotation); - if (sourceRectangle.Contains(rotated.X, rotated.Y)) - { - target[x, y] = source[rotated.X, rotated.Y]; - } + target[x, y] = this.firstPass[rotated.X, rotated.Y]; } - this.OnRowProcessed(); } + + this.OnRowProcessed(); }); } + + /// + protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + // Cleanup. + this.firstPass.Dispose(); + } } } \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs index 2516cf62b8..845bf0b1b6 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs @@ -76,6 +76,34 @@ } } + [Fact] + public void ImageShouldPad() + { + if (!Directory.Exists("TestOutput/Pad")) + { + Directory.CreateDirectory("TestOutput/Pad"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + + string filename = Path.GetFileName(file); + + using (Image image = new Image(stream)) + using (FileStream output = File.OpenWrite($"TestOutput/Pad/{filename}")) + { + image.Pad(image.Width + 50, image.Height + 50, this.ProgressUpdate) + .Save(output); + } + + Trace.WriteLine($"{watch.ElapsedMilliseconds}ms"); + } + } + } + [Theory] [MemberData("ReSamplers")] public void ImageShouldResize(string name, IResampler sampler)