diff --git a/src/ImageProcessor.UnitTests/ImageFactoryUnitTests.cs b/src/ImageProcessor.UnitTests/ImageFactoryUnitTests.cs index 9f42ba3f33..e4eb9db824 100644 --- a/src/ImageProcessor.UnitTests/ImageFactoryUnitTests.cs +++ b/src/ImageProcessor.UnitTests/ImageFactoryUnitTests.cs @@ -466,6 +466,38 @@ namespace ImageProcessor.UnitTests } } + /// + /// Tests that the image's inside is rotated + /// + [Test] + public void ImageIsRotatedInside() + { + foreach (ImageFactory imageFactory in this.ListInputImages()) + { + Image original = (Image)imageFactory.Image.Clone(); + imageFactory.RotateInside(new RotateInsideLayer { Angle = 45, KeepImageDimensions = true }); + + imageFactory.Image.Width.Should().Be(original.Width, "because the rotated image dimensions should not have changed"); + imageFactory.Image.Height.Should().Be(original.Height, "because the rotated image dimensions should not have changed"); + } + } + + /// + /// Tests that the image's inside is rotated and resized + /// + [Test] + public void ImageIsRotatedInsideAndResized() + { + foreach (ImageFactory imageFactory in this.ListInputImages()) + { + Image original = (Image)imageFactory.Image.Clone(); + imageFactory.RotateInside(new RotateInsideLayer { Angle = 45, KeepImageDimensions = false }); + + imageFactory.Image.Width.Should().NotBe(original.Width, "because the rotated image dimensions should have changed"); + imageFactory.Image.Height.Should().NotBe(original.Height, "because the rotated image dimensions should have changed"); + } + } + /// /// Tests that the images hue has been altered. /// diff --git a/src/ImageProcessor.UnitTests/ImageProcessor.UnitTests.csproj b/src/ImageProcessor.UnitTests/ImageProcessor.UnitTests.csproj index b1ae1daaf5..1d808f9637 100644 --- a/src/ImageProcessor.UnitTests/ImageProcessor.UnitTests.csproj +++ b/src/ImageProcessor.UnitTests/ImageProcessor.UnitTests.csproj @@ -64,6 +64,7 @@ + diff --git a/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs b/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs new file mode 100644 index 0000000000..3e1ffdfb36 --- /dev/null +++ b/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs @@ -0,0 +1,54 @@ +namespace ImageProcessor.UnitTests.Imaging.Helpers +{ + using System.Drawing; + using FluentAssertions; + using ImageProcessor.Imaging.Helpers; + using NUnit.Framework; + + /// + /// Test harness for the image math unit tests + /// + public class ImageMathsUnitTests + { + /// + /// Tests that the bounding rectangle of a rotated image is calculated + /// + /// The width of the image. + /// The height of the image. + /// The rotation angle. + /// The expected width. + /// The expected height. + [Test] + [TestCase(100, 100, 45, 141, 141)] + [TestCase(100, 100, 30, 137, 137)] + [TestCase(100, 200, 50, 217, 205)] + public void BoundingRotatedRectangleIsCalculated(int width, int height, float angle, int expectedWidth, int expectedHeight) + { + Rectangle result = ImageMaths.GetBoundingRotatedRectangle(width, height, angle); + + result.Width.Should().Be(expectedWidth, "because the rotated width should have been calculated"); + result.Height.Should().Be(expectedHeight, "because the rotated height should have been calculated"); + } + + /// + /// Tests that the zoom needed for an "inside" rotation is calculated + /// + /// Width of the image. + /// Height of the image. + /// The rotation angle. + /// The expected zoom. + [Test] + [TestCase(100, 100, 45, 1.41f)] + [TestCase(100, 100, 15, 1.22f)] + [TestCase(100, 200, 45, 2.12f)] + [TestCase(200, 100, 45, 2.12f)] + [TestCase(600, 450, 20, 1.39f)] + [TestCase(600, 450, 45, 1.64f)] + public void RotationZoomIsCalculated(int imageWidth, int imageHeight, float angle, float expected) + { + float result = ImageMaths.ZoomAfterRotation(imageWidth, imageHeight, angle); + + result.Should().BeApproximately(expected, 0.01f, "because the zoom level after rotation should have been calculated"); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 6ac5e542eb..dd2e51e3da 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -895,6 +895,24 @@ namespace ImageProcessor return this; } + /// + /// Rotates the image inside its area; keeps the area straight. + /// + /// The rotation layer parameters. + /// + /// The current instance of the class. + /// + public ImageFactory RotateInside(RotateInsideLayer rotateLayer) + { + if (this.ShouldProcess) + { + RotateInside rotate = new RotateInside { DynamicParameter = rotateLayer }; + this.CurrentImageFormat.ApplyProcessor(rotate.ProcessImage, this); + } + + return this; + } + /// /// Adds rounded corners to the current image. /// diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 88d010e60e..aeb5403efe 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -214,6 +214,7 @@ + @@ -233,6 +234,7 @@ + diff --git a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs index af05939762..76ed51d777 100644 --- a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs +++ b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs @@ -12,7 +12,6 @@ namespace ImageProcessor.Imaging.Helpers { using System; using System.Drawing; - using ImageProcessor.Imaging.Colors; /// @@ -20,6 +19,27 @@ namespace ImageProcessor.Imaging.Helpers /// public static class ImageMaths { + /// + /// Gets a representing the child centered relative to the parent. + /// + /// + /// The parent . + /// + /// + /// The child . + /// + /// + /// The centered . + /// + public static RectangleF CenteredRectangle(Rectangle parent, Rectangle child) + { + float x = (parent.Width - child.Width) / 2.0F; + float y = (parent.Height - child.Height) / 2.0F; + int width = child.Width; + int height = child.Height; + return new RectangleF(x, y, width, height); + } + /// /// Restricts a value to be within a specified range. /// @@ -53,6 +73,20 @@ namespace ImageProcessor.Imaging.Helpers return value; } + /// + /// Returns the given degrees converted to radians. + /// + /// + /// The angle in degrees. + /// + /// + /// The representing the degree as radians. + /// + public static double DegreesToRadians(double angleInDegrees) + { + return angleInDegrees * (Math.PI / 180); + } + /// /// Gets the bounding from the given points. /// @@ -70,6 +104,40 @@ namespace ImageProcessor.Imaging.Helpers return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); } + /// + /// Calculates the new size after rotation. + /// + /// The width of the image. + /// The height of the image. + /// The angle of rotation. + /// The new size of the image + public static Rectangle GetBoundingRotatedRectangle(int width, int height, float angle) + { + double widthAsDouble = width; + double heightAsDouble = height; + + double radians = DegreesToRadians(angle); + double radiansSin = Math.Sin(radians); + double radiansCos = Math.Cos(radians); + double width1 = (heightAsDouble * radiansSin) + (widthAsDouble * radiansCos); + double height1 = (widthAsDouble * radiansSin) + (heightAsDouble * radiansCos); + + // Find dimensions in the other direction + radiansSin = Math.Sin(-radians); + radiansCos = Math.Cos(-radians); + double width2 = (heightAsDouble * radiansSin) + (widthAsDouble * radiansCos); + double height2 = (widthAsDouble * radiansSin) + (heightAsDouble * radiansCos); + + // Get the external vertex for the rotation + Rectangle result = new Rectangle( + 0, + 0, + Convert.ToInt32(Math.Max(Math.Abs(width1), Math.Abs(width2))), + Convert.ToInt32(Math.Max(Math.Abs(height1), Math.Abs(height2)))); + + return result; + } + /// /// Finds the bounding rectangle based on the first instance of any color component other /// than the given one. @@ -101,12 +169,15 @@ namespace ImageProcessor.Imaging.Helpers case RgbaComponent.R: delegateFunc = (fastBitmap, x, y, b) => fastBitmap.GetPixel(x, y).R != b; break; + case RgbaComponent.G: delegateFunc = (fastBitmap, x, y, b) => fastBitmap.GetPixel(x, y).G != b; break; + case RgbaComponent.A: delegateFunc = (fastBitmap, x, y, b) => fastBitmap.GetPixel(x, y).A != b; break; + default: delegateFunc = (fastBitmap, x, y, b) => fastBitmap.GetPixel(x, y).B != b; break; @@ -188,24 +259,31 @@ namespace ImageProcessor.Imaging.Helpers } /// - /// Gets a representing the child centered relative to the parent. + /// Rotates one point around another + /// /// - /// - /// The parent . - /// - /// - /// The child . + /// The point to rotate. + /// The rotation angle in degrees. + /// The centre point of rotation. If not set the point will equal + /// /// - /// - /// The centered . - /// - public static RectangleF CenteredRectangle(Rectangle parent, Rectangle child) + /// Rotated point + public static Point RotatePoint(Point pointToRotate, double angleInDegrees, Point? centerPoint = null) { - float x = (parent.Width - child.Width) / 2.0F; - float y = (parent.Height - child.Height) / 2.0F; - int width = child.Width; - int height = child.Height; - return new RectangleF(x, y, width, height); + Point center = centerPoint ?? Point.Empty; + + double angleInRadians = DegreesToRadians(angleInDegrees); + double cosTheta = Math.Cos(angleInRadians); + double sinTheta = Math.Sin(angleInRadians); + return new Point + { + X = + (int)((cosTheta * (pointToRotate.X - center.X)) - + ((sinTheta * (pointToRotate.Y - center.Y)) + center.X)), + Y = + (int)((sinTheta * (pointToRotate.X - center.X)) + + ((cosTheta * (pointToRotate.Y - center.Y)) + center.Y)) + }; } /// @@ -221,55 +299,33 @@ namespace ImageProcessor.Imaging.Helpers { return new[] { - new Point(rectangle.Left, rectangle.Top), - new Point(rectangle.Right, rectangle.Top), - new Point(rectangle.Right, rectangle.Bottom), + new Point(rectangle.Left, rectangle.Top), + new Point(rectangle.Right, rectangle.Top), + new Point(rectangle.Right, rectangle.Bottom), new Point(rectangle.Left, rectangle.Bottom) }; } /// - /// Returns the given degrees converted to radians. + /// Calculates the zoom needed after the rotation. /// - /// - /// The angle in degrees. - /// - /// - /// The representing the degree as radians. - /// - public static double DegreesToRadians(double angleInDegrees) + /// Width of the image. + /// Height of the image. + /// The angle. + /// + /// Based on + /// + /// The zoom needed + public static float ZoomAfterRotation(int imageWidth, int imageHeight, float angle) { - return angleInDegrees * (Math.PI / 180); - } + double radians = angle * Math.PI / 180d; + double radiansSin = Math.Sin(radians); + double radiansCos = Math.Cos(radians); - /// - /// Rotates one point around another - /// - /// - /// The point to rotate. - /// The rotation angle in degrees. - /// The centre point of rotation. If not set the point will equal - /// - /// - /// Rotated point - public static Point RotatePoint(Point pointToRotate, double angleInDegrees, Point? centerPoint = null) - { - Point center = centerPoint ?? Point.Empty; + double widthRotated = (imageWidth * radiansCos) + (imageHeight * radiansSin); + double heightRotated = (imageWidth * radiansSin) + (imageHeight * radiansCos); - double angleInRadians = DegreesToRadians(angleInDegrees); - double cosTheta = Math.Cos(angleInRadians); - double sinTheta = Math.Sin(angleInRadians); - return new Point - { - X = - (int) - ((cosTheta * (pointToRotate.X - center.X)) - - ((sinTheta * (pointToRotate.Y - center.Y)) + center.X)), - Y = - (int) - ((sinTheta * (pointToRotate.X - center.X)) + - ((cosTheta * (pointToRotate.Y - center.Y)) + center.Y)) - }; + return (float)Math.Max(widthRotated / imageWidth, heightRotated / imageHeight); } } -} +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/RotateInsideLayer.cs b/src/ImageProcessor/Imaging/RotateInsideLayer.cs new file mode 100644 index 0000000000..969c94a6e7 --- /dev/null +++ b/src/ImageProcessor/Imaging/RotateInsideLayer.cs @@ -0,0 +1,30 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates the properties required to add an rotation layer to an image. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging +{ + /// + /// A rotation layer to apply an inside rotation to an image + /// + public class RotateInsideLayer + { + /// + /// Gets or sets the rotation angle. + /// + public float Angle { get; set; } + + /// + /// Gets or sets a value indicating whether to keep the image dimensions. + /// If set to true, the image is zoomed inside the area. + /// If set to false, the area is resized to match the rotated image. + /// + public bool KeepImageDimensions { get; set; } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Processors/Rotate.cs b/src/ImageProcessor/Processors/Rotate.cs index 2cd57a665e..9d51bbc8d4 100644 --- a/src/ImageProcessor/Processors/Rotate.cs +++ b/src/ImageProcessor/Processors/Rotate.cs @@ -104,30 +104,13 @@ namespace ImageProcessor.Processors /// private Bitmap RotateImage(Image image, float rotateAtX, float rotateAtY, float angle) { - double widthAsDouble = image.Width; - double heightAsDouble = image.Height; + Rectangle newSize = Imaging.Helpers.ImageMaths.GetBoundingRotatedRectangle(image.Width, image.Height, angle); - double radians = angle * Math.PI / 180d; - double radiansSin = Math.Sin(radians); - double radiansCos = Math.Cos(radians); - double width1 = (heightAsDouble * radiansSin) + (widthAsDouble * radiansCos); - double height1 = (widthAsDouble * radiansSin) + (heightAsDouble * radiansCos); - - // Find dimensions in the other direction - radiansSin = Math.Sin(-radians); - radiansCos = Math.Cos(-radians); - double width2 = (heightAsDouble * radiansSin) + (widthAsDouble * radiansCos); - double height2 = (widthAsDouble * radiansSin) + (heightAsDouble * radiansCos); - - // Get the external vertex for the rotation - int width = Convert.ToInt32(Math.Max(Math.Abs(width1), Math.Abs(width2))); - int height = Convert.ToInt32(Math.Max(Math.Abs(height1), Math.Abs(height2))); - - int x = (width - image.Width) / 2; - int y = (height - image.Height) / 2; + int x = (newSize.Width - image.Width) / 2; + int y = (newSize.Height - image.Height) / 2; // Create a new empty bitmap to hold rotated image - Bitmap newImage = new Bitmap(width, height); + Bitmap newImage = new Bitmap(newSize.Width, newSize.Height); newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); // Make a graphics object from the empty bitmap diff --git a/src/ImageProcessor/Processors/RotateInside.cs b/src/ImageProcessor/Processors/RotateInside.cs new file mode 100644 index 0000000000..8d4436cdab --- /dev/null +++ b/src/ImageProcessor/Processors/RotateInside.cs @@ -0,0 +1,153 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates methods to rotate the inside of an image. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Processors +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Drawing.Drawing2D; + using ImageProcessor.Common.Exceptions; + using ImageProcessor.Imaging; + + /// + /// Encapsulates the methods to rotate the inside of an image + /// + public class RotateInside : IGraphicsProcessor + { + /// + /// Gets or sets the DynamicParameter. + /// + public dynamic DynamicParameter { get; set; } + + /// + /// Gets or sets any additional settings required by the processor. + /// + public Dictionary Settings { get; set; } + + /// + /// Processes the image. + /// + /// The current instance of the class containing + /// the image to process. + /// + /// The processed image from the current instance of the class. + /// + /// + /// Based on + /// + public Image ProcessImage(ImageFactory factory) + { + Bitmap newImage = null; + Image image = factory.Image; + + try + { + RotateInsideLayer rotateLayer = this.DynamicParameter; + + // Create a rotated image. + newImage = this.RotateImage(image, rotateLayer); + + image.Dispose(); + image = newImage; + } + catch (Exception ex) + { + if (newImage != null) + { + newImage.Dispose(); + } + + throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex); + } + + return image; + } + + /// + /// Rotates the inside of an image to the given angle at the given position. + /// + /// The image to rotate + /// The rotation layer. + /// + /// Based on the Rotate effect + /// + /// The image rotated to the given angle at the given position. + private Bitmap RotateImage(Image image, RotateInsideLayer rotateLayer) + { + Size newSize = new Size(image.Width, image.Height); + + float zoom = Imaging.Helpers.ImageMaths.ZoomAfterRotation(image.Width, image.Height, rotateLayer.Angle); + + // if we don't keep the image dimensions, calculate the new ones + if (!rotateLayer.KeepImageDimensions) + { + newSize.Width = (int)(newSize.Width / zoom); + newSize.Height = (int)(newSize.Height / zoom); + } + + // Center of the image + float rotateAtX = Math.Abs(image.Width / 2); + float rotateAtY = Math.Abs(image.Height / 2); + + // Create a new empty bitmap to hold rotated image + Bitmap newImage = new Bitmap(newSize.Width, newSize.Height); + newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); + + // Make a graphics object from the empty bitmap + using (Graphics graphics = Graphics.FromImage(newImage)) + { + // Reduce the jagged edge. + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + + if (rotateLayer.KeepImageDimensions) + { + // Put the rotation point in the "center" of the image + graphics.TranslateTransform(rotateAtX, rotateAtY); + + // Rotate the image + graphics.RotateTransform(rotateLayer.Angle); + + // Zooms the image to fit the area + graphics.ScaleTransform(zoom, zoom); + + // Move the image back + graphics.TranslateTransform(-rotateAtX, -rotateAtY); + + // Draw passed in image onto graphics object + graphics.DrawImage(image, new PointF(0, 0)); + } + else + { + // calculate the difference between the center of the original image and the center of the new image + int diffX = (image.Width - newSize.Width) / 2; + int diffY = (image.Height - newSize.Height) / 2; + + // Put the rotation point in the "center" of the old image + graphics.TranslateTransform(rotateAtX - diffX, rotateAtY - diffY); + + // Rotate the image + graphics.RotateTransform(rotateLayer.Angle); + + // Move the image back + graphics.TranslateTransform(-(rotateAtX - diffX), -(rotateAtY - diffY)); + + // Draw passed in image onto graphics object + graphics.DrawImage(image, new PointF(-diffX, -diffY)); + } + } + + return newImage; + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Properties/AssemblyInfo.cs b/src/ImageProcessor/Properties/AssemblyInfo.cs index 2f753e3b7e..e198b5672e 100644 --- a/src/ImageProcessor/Properties/AssemblyInfo.cs +++ b/src/ImageProcessor/Properties/AssemblyInfo.cs @@ -9,6 +9,7 @@ // -------------------------------------------------------------------------------------------------------------------- using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -42,3 +43,5 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: [assembly: AssemblyVersion("2.2.0.0")] [assembly: AssemblyFileVersion("2.2.0.0")] + +[assembly: InternalsVisibleTo("ImageProcessor.UnitTests")] \ No newline at end of file