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