From 2bbdfbea1d52b1a4a4f93687f89f173e4603e32d Mon Sep 17 00:00:00 2001 From: James South Date: Sun, 1 Dec 2013 12:26:11 +0000 Subject: [PATCH] Adding pad functionality to resize Former-commit-id: 745db104546ed4a845bc996a74de405f186a5d1e --- src/ImageProcessor/ImageFactory.cs | 2 - src/ImageProcessor/ImageProcessor.csproj | 2 + src/ImageProcessor/Imaging/ResizeLayer.cs | 115 +++++++++++++ src/ImageProcessor/Imaging/ResizeMode.cs | 28 ++++ src/ImageProcessor/Processors/Constrain.cs | 4 +- src/ImageProcessor/Processors/Resize.cs | 151 ++++++++++++++++-- src/ImageProcessor/Processors/ResizeBase.cs | 66 ++++++-- src/ImageProcessor/Processors/Rotate.cs | 2 +- src/ImageProcessor/Settings.StyleCop | 5 + .../Views/Home/Index.cshtml | 14 ++ 10 files changed, 356 insertions(+), 33 deletions(-) create mode 100644 src/ImageProcessor/Imaging/ResizeLayer.cs create mode 100644 src/ImageProcessor/Imaging/ResizeMode.cs diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 8072d5731..429e4ff44 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -375,8 +375,6 @@ namespace ImageProcessor return this; } - - /// /// Crops the current image to the given location and size. /// diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 1efd252d1..ea024b3a6 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -62,6 +62,7 @@ + @@ -75,6 +76,7 @@ + diff --git a/src/ImageProcessor/Imaging/ResizeLayer.cs b/src/ImageProcessor/Imaging/ResizeLayer.cs new file mode 100644 index 000000000..026990fe9 --- /dev/null +++ b/src/ImageProcessor/Imaging/ResizeLayer.cs @@ -0,0 +1,115 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates the properties required to resize an image. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging +{ + #region Using + using System.Drawing; + #endregion + + /// + /// Encapsulates the properties required to resize an image. + /// + public class ResizeLayer + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public ResizeLayer() + { + this.ResizeMode = ResizeMode.Pad; + this.BackgroundColor = Color.Transparent; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The resize mode to apply to resized image. + /// + public ResizeLayer(ResizeMode resizeMode) + { + this.ResizeMode = resizeMode; + this.BackgroundColor = Color.Transparent; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The resize mode to apply to resized image. + /// + /// + /// The to set as the background color. + /// Used for image formats that do not support transparency + /// + public ResizeLayer(ResizeMode resizeMode, Color backgroundColor) + { + this.ResizeMode = resizeMode; + this.BackgroundColor = backgroundColor; + } + #endregion + + #region Properties + /// + /// Gets or sets the size. + /// + public Size Size { get; set; } + + /// + /// Gets or sets a value indicating whether to ResizeMode the layer. + /// + public ResizeMode ResizeMode { get; set; } + + /// + /// Gets or sets the background color. + /// + public Color BackgroundColor { get; set; } + #endregion + + /// + /// Returns a value that indicates whether the specified object is an + /// object that is equivalent to + /// this object. + /// + /// + /// The object to test. + /// + /// + /// True if the given object is an object that is equivalent to + /// this object; otherwise, false. + /// + public override bool Equals(object obj) + { + ResizeLayer resizeLayer = obj as ResizeLayer; + + if (resizeLayer == null) + { + return false; + } + + return this.Size == resizeLayer.Size + && this.ResizeMode == resizeLayer.ResizeMode + && this.BackgroundColor == resizeLayer.BackgroundColor; + } + + /// + /// Returns a hash code value that represents this object. + /// + /// + /// A hash code that represents this object. + /// + public override int GetHashCode() + { + return this.Size.GetHashCode() + this.ResizeMode.GetHashCode() + this.BackgroundColor.GetHashCode(); + } + } +} diff --git a/src/ImageProcessor/Imaging/ResizeMode.cs b/src/ImageProcessor/Imaging/ResizeMode.cs new file mode 100644 index 000000000..144a5a485 --- /dev/null +++ b/src/ImageProcessor/Imaging/ResizeMode.cs @@ -0,0 +1,28 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Enumerated resize modes to apply to resized images. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging +{ + /// + /// Enumerated resize modes to apply to resized images. + /// + public enum ResizeMode + { + /// + /// Pads the resized image to fit the bounds of its container. + /// + Pad, + + /// + /// Stretches the resized image to fit the bounds of its container. + /// + Stretch + } +} diff --git a/src/ImageProcessor/Processors/Constrain.cs b/src/ImageProcessor/Processors/Constrain.cs index e2aaafaea..51528b05d 100644 --- a/src/ImageProcessor/Processors/Constrain.cs +++ b/src/ImageProcessor/Processors/Constrain.cs @@ -15,6 +15,8 @@ namespace ImageProcessor.Processors using System.Drawing; using System.Text.RegularExpressions; using ImageProcessor.Helpers.Extensions; + using ImageProcessor.Imaging; + #endregion /// @@ -126,7 +128,7 @@ namespace ImageProcessor.Processors int.TryParse(this.Settings["MaxWidth"], out defaultMaxWidth); int.TryParse(this.Settings["MaxHeight"], out defaultMaxHeight); - return this.ResizeImage(factory, newSize.Width, newSize.Height, defaultMaxWidth, defaultMaxHeight); + return this.ResizeImage(factory, newSize.Width, newSize.Height, defaultMaxWidth, defaultMaxHeight, ResizeMode.Pad, Color.Transparent); } return factory.Image; diff --git a/src/ImageProcessor/Processors/Resize.cs b/src/ImageProcessor/Processors/Resize.cs index dccaeb583..1b6a73104 100644 --- a/src/ImageProcessor/Processors/Resize.cs +++ b/src/ImageProcessor/Processors/Resize.cs @@ -11,10 +11,15 @@ namespace ImageProcessor.Processors { #region Using + + using System; using System.Collections.Generic; using System.Drawing; + using System.Text; using System.Text.RegularExpressions; using ImageProcessor.Helpers.Extensions; + using ImageProcessor.Imaging; + #endregion /// @@ -25,7 +30,22 @@ namespace ImageProcessor.Processors /// /// The regular expression to search strings for. /// - private static readonly Regex QueryRegex = new Regex(@"(width|height)=\d+", RegexOptions.Compiled); + private static readonly Regex QueryRegex = new Regex(@"((width|height)=\d+)|(mode=(pad|stretch|crop))|(bgcolor-([0-9a-fA-F]{3}){1,2})", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the size attribute. + /// + private static readonly Regex SizeRegex = new Regex(@"(width|height)=\d+"); + + /// + /// The regular expression to search strings for the mode attribute. + /// + private static readonly Regex ModeRegex = new Regex(@"mode=(pad|stretch|crop)"); + + /// + /// The regular expression to search strings for the color attribute. + /// + private static readonly Regex ColorRegex = new Regex(@"bgcolor-([0-9a-fA-F]{3}){1,2}", RegexOptions.Compiled); #region IGraphicsProcessor Members /// @@ -81,7 +101,10 @@ namespace ImageProcessor.Processors // Set the sort order to max to allow filtering. this.SortOrder = int.MaxValue; - Size size = new Size(); + ResizeLayer resizeLayer = new ResizeLayer(); + + // First merge the matches so we can parse . + StringBuilder stringBuilder = new StringBuilder(); foreach (Match match in this.RegexPattern.Matches(queryString)) { @@ -93,21 +116,20 @@ namespace ImageProcessor.Processors this.SortOrder = match.Index; } - // Match syntax - if (match.Value.Contains("width")) - { - size.Width = match.Value.ToPositiveIntegerArray()[0]; - } - else - { - size.Height = match.Value.ToPositiveIntegerArray()[0]; - } + stringBuilder.Append(match.Value); index += 1; } } - this.DynamicParameter = size; + // Match syntax + string toParse = stringBuilder.ToString(); + + resizeLayer.Size = this.ParseSize(toParse); + resizeLayer.ResizeMode = this.ParseMode(toParse); + resizeLayer.BackgroundColor = this.ParseColor(toParse); + + this.DynamicParameter = resizeLayer; return this.SortOrder; } @@ -123,16 +145,115 @@ namespace ImageProcessor.Processors /// public override Image ProcessImage(ImageFactory factory) { - int width = this.DynamicParameter.Width ?? 0; - int height = this.DynamicParameter.Height ?? 0; + int width = this.DynamicParameter.Size.Width ?? 0; + int height = this.DynamicParameter.Size.Height ?? 0; + ResizeMode mode = this.DynamicParameter.ResizeMode; + Color backgroundColor = this.DynamicParameter.BackgroundColor; int defaultMaxWidth; int defaultMaxHeight; int.TryParse(this.Settings["MaxWidth"], out defaultMaxWidth); int.TryParse(this.Settings["MaxHeight"], out defaultMaxHeight); - return this.ResizeImage(factory, width, height, defaultMaxWidth, defaultMaxHeight); + return this.ResizeImage(factory, width, height, defaultMaxWidth, defaultMaxHeight, mode, backgroundColor); } #endregion + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The . + /// + private Size ParseSize(string input) + { + const string Width = "width"; + const string Height = "height"; + Size size = new Size(); + + // First merge the matches so we can parse . + StringBuilder stringBuilder = new StringBuilder(); + foreach (Match match in SizeRegex.Matches(input)) + { + stringBuilder.Append(match.Value); + } + + // First cater for single dimensions. + string value = stringBuilder.ToString(); + + if (input.Contains(Width) && !input.Contains(Height)) + { + size = new Size(value.ToPositiveIntegerArray()[0], 0); + } + + if (input.Contains(Height) && !input.Contains(Width)) + { + size = new Size(0, value.ToPositiveIntegerArray()[0]); + } + + // Both dimensions supplied. + if (input.Contains(Height) && input.Contains(Width)) + { + int[] dimensions = value.ToPositiveIntegerArray(); + + // Check the order in which they have been supplied. + size = input.IndexOf(Width, StringComparison.Ordinal) < input.IndexOf(Height, StringComparison.Ordinal) + ? new Size(dimensions[0], dimensions[1]) + : new Size(dimensions[1], dimensions[0]); + } + + return size; + } + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The correct . + /// + private ResizeMode ParseMode(string input) + { + foreach (Match match in ModeRegex.Matches(input)) + { + // Split on = + string mode = match.Value.Split('=')[1]; + + switch (mode) + { + case "stretch": + return ResizeMode.Stretch; + default: + return ResizeMode.Pad; + } + } + + return ResizeMode.Pad; + } + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The correct + /// + private Color ParseColor(string input) + { + foreach (Match match in ColorRegex.Matches(input)) + { + // split on color-hex + return ColorTranslator.FromHtml("#" + match.Value.Split('-')[1]); + } + + return Color.Transparent; + } } } diff --git a/src/ImageProcessor/Processors/ResizeBase.cs b/src/ImageProcessor/Processors/ResizeBase.cs index fce23eca5..e0405d070 100644 --- a/src/ImageProcessor/Processors/ResizeBase.cs +++ b/src/ImageProcessor/Processors/ResizeBase.cs @@ -17,6 +17,9 @@ namespace ImageProcessor.Processors using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Text.RegularExpressions; + + using ImageProcessor.Imaging; + #endregion /// @@ -87,10 +90,16 @@ namespace ImageProcessor.Processors /// /// The default max height to resize the image to. /// + /// + /// Whether to pad the image to fill the set size. + /// + /// + /// The background color to pad the image with. + /// /// /// The processed image from the current instance of the class. /// - protected Image ResizeImage(ImageFactory factory, int width, int height, int defaultMaxWidth, int defaultMaxHeight) + protected Image ResizeImage(ImageFactory factory, int width, int height, int defaultMaxWidth, int defaultMaxHeight, ResizeMode resizeMode, Color backgroundColor) { Bitmap newImage = null; Image image = factory.Image; @@ -100,26 +109,56 @@ namespace ImageProcessor.Processors int sourceWidth = image.Width; int sourceHeight = image.Height; + int destinationWidth = width; + int destinationHeight = height; + int maxWidth = defaultMaxWidth > 0 ? defaultMaxWidth : int.MaxValue; int maxHeight = defaultMaxHeight > 0 ? defaultMaxHeight : int.MaxValue; + // Fractional variants for preserving aspect ratio. + double percentHeight = Math.Abs(height / (double)sourceHeight); + double percentWidth = Math.Abs(width / (double)sourceWidth); + + int destinationX = 0; + int destinationY = 0; + + // Change the destination rectangle coordinates if padding and + // there has been a set width and height. + if (resizeMode == ResizeMode.Pad && width > 0 && height > 0) + { + double ratio; + + if (percentHeight < percentWidth) + { + ratio = percentHeight; + destinationX = (int)((width - (sourceWidth * ratio)) / 2); + destinationWidth = (int)Math.Floor(sourceWidth * percentHeight); + } + else + { + ratio = percentWidth; + destinationY = (int)((height - (sourceHeight * ratio)) / 2); + destinationHeight = (int)Math.Floor(sourceHeight * percentWidth); + } + } + // If height or width is not passed we assume that the standard ratio is to be kept. if (height == 0) { - // Bit of simple fractional maths here. - float percentWidth = Math.Abs(width / (float)sourceWidth); - height = (int)Math.Floor(sourceHeight * percentWidth); + destinationHeight = (int)Math.Floor(sourceHeight * percentWidth); + height = destinationHeight; } if (width == 0) { - float percentHeight = Math.Abs(height / (float)sourceHeight); - width = (int)Math.Floor(sourceWidth * percentHeight); + destinationWidth = (int)Math.Floor(sourceWidth * percentHeight); + width = destinationWidth; } if (width > 0 && height > 0 && width <= maxWidth && height <= maxHeight) { - // Dont use an object initializer here. + // Don't use an object initializer here. + // ReSharper disable once UseObjectOrCollectionInitializer newImage = new Bitmap(width, height, PixelFormat.Format32bppPArgb); newImage.Tag = image.Tag; @@ -128,7 +167,7 @@ namespace ImageProcessor.Processors // We want to use two different blending algorithms for enlargement/shrinking. // Bicubic is better enlarging for whilst Bilinear is better for shrinking. // http://www.codinghorror.com/blog/2007/07/better-image-resizing.html - if (image.Width < width && image.Height < height) + if (image.Width < destinationWidth && image.Height < destinationHeight) { // We are making it larger. graphics.SmoothingMode = SmoothingMode.AntiAlias; @@ -140,11 +179,9 @@ namespace ImageProcessor.Processors { // We are making it smaller. graphics.SmoothingMode = SmoothingMode.None; - - // Contrary to everything I have read bicubic is producing the best results. - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.PixelOffsetMode = PixelOffsetMode.None; - graphics.CompositingQuality = CompositingQuality.HighSpeed; + graphics.InterpolationMode = InterpolationMode.HighQualityBilinear; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; } // An unwanted border appears when using InterpolationMode.HighQualityBicubic to resize the image @@ -154,7 +191,8 @@ namespace ImageProcessor.Processors using (ImageAttributes wrapMode = new ImageAttributes()) { wrapMode.SetWrapMode(WrapMode.TileFlipXY); - Rectangle destRect = new Rectangle(0, 0, width, height); + graphics.Clear(backgroundColor); + Rectangle destRect = new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); graphics.DrawImage(image, destRect, 0, 0, sourceWidth, sourceHeight, GraphicsUnit.Pixel, wrapMode); } diff --git a/src/ImageProcessor/Processors/Rotate.cs b/src/ImageProcessor/Processors/Rotate.cs index 831250700..ec7094393 100644 --- a/src/ImageProcessor/Processors/Rotate.cs +++ b/src/ImageProcessor/Processors/Rotate.cs @@ -24,7 +24,7 @@ namespace ImageProcessor.Processors /// /// The regular expression to search strings for. /// - private static readonly Regex QueryRegex = new Regex(@"rotate=((?:3[0-5][0-9]|[12][0-9]{2}|[1-9][0-9]?)|angle-(?:3[0-5][0-9]|[12][0-9]{2}|[1-9][0-9]?)\|bgcolor-([0-9a-fA-F]{3}){1,2})", RegexOptions.Compiled); + private static readonly Regex QueryRegex = new Regex(@"rotate=((?:3[0-5][0-9]|[12][0-9]{2}|[1-9][0-9]?)|angle-(?:3[0-5][0-9]|[12][0-9]{2}|[1-9][0-9]?)[\|,]bgcolor-([0-9a-fA-F]{3}){1,2})", RegexOptions.Compiled); /// /// The regular expression to search strings for the angle attribute. diff --git a/src/ImageProcessor/Settings.StyleCop b/src/ImageProcessor/Settings.StyleCop index 253825567..0b178ac1e 100644 --- a/src/ImageProcessor/Settings.StyleCop +++ b/src/ImageProcessor/Settings.StyleCop @@ -1,4 +1,9 @@ + + + behaviour + + diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml index 1f6ef52b7..0ac209e64 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml +++ b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml @@ -16,6 +16,20 @@ + +
+
+
+

Padded

+ + @*

Foreign language test.

+ *@ +
+
+ @*

Cropped

+ *@ +
+

Filter