diff --git a/src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs b/src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs
index e4657511a..05b71bb2f 100644
--- a/src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs
+++ b/src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs
@@ -33,7 +33,7 @@ namespace ImageProcessorCore
result = new byte[bytes.Length * 8 / bits];
// BUGFIX I dont think it should be there, but I am not sure if it breaks something else
- //int factor = (int)Math.Pow(2, bits) - 1;
+ // int factor = (int)Math.Pow(2, bits) - 1;
int mask = 0xFF >> (8 - bits);
int resultOffset = 0;
@@ -41,7 +41,7 @@ namespace ImageProcessorCore
{
for (int shift = 0; shift < 8; shift += bits)
{
- int colorIndex = ((b >> (8 - bits - shift)) & mask); // * (255 / factor);
+ int colorIndex = (b >> (8 - bits - shift)) & mask; // * (255 / factor);
result[resultOffset] = (byte)colorIndex;
diff --git a/src/ImageProcessorCore/ParallelImageProcessor.cs b/src/ImageProcessorCore/ParallelImageProcessor.cs
index 297f796f8..8ec38d125 100644
--- a/src/ImageProcessorCore/ParallelImageProcessor.cs
+++ b/src/ImageProcessorCore/ParallelImageProcessor.cs
@@ -93,9 +93,13 @@ namespace ImageProcessorCore
sourceRectangle = source.Bounds;
}
- this.OnApply(source, target, target.Bounds, sourceRectangle);
+ if (targetRectangle == Rectangle.Empty)
+ {
+ targetRectangle = target.Bounds;
+ }
+
+ this.OnApply(source, target, targetRectangle, sourceRectangle);
- targetRectangle = target.Bounds;
this.numRowsProcessed = 0;
this.totalRows = targetRectangle.Bottom;
diff --git a/src/ImageProcessorCore/Samplers/AnchorPosition.cs b/src/ImageProcessorCore/Samplers/AnchorPosition.cs
new file mode 100644
index 000000000..ee9e7255f
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/AnchorPosition.cs
@@ -0,0 +1,58 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Samplers
+{
+ ///
+ /// Enumerated anchor positions to apply to resized images.
+ ///
+ public enum AnchorPosition
+ {
+ ///
+ /// Anchors the position of the image to the center of it's bounding container.
+ ///
+ Center,
+
+ ///
+ /// Anchors the position of the image to the top of it's bounding container.
+ ///
+ Top,
+
+ ///
+ /// Anchors the position of the image to the bottom of it's bounding container.
+ ///
+ Bottom,
+
+ ///
+ /// Anchors the position of the image to the left of it's bounding container.
+ ///
+ Left,
+
+ ///
+ /// Anchors the position of the image to the right of it's bounding container.
+ ///
+ Right,
+
+ ///
+ /// Anchors the position of the image to the top left side of it's bounding container.
+ ///
+ TopLeft,
+
+ ///
+ /// Anchors the position of the image to the top right side of it's bounding container.
+ ///
+ TopRight,
+
+ ///
+ /// Anchors the position of the image to the bottom right side of it's bounding container.
+ ///
+ BottomRight,
+
+ ///
+ /// Anchors the position of the image to the bottom left side of it's bounding container.
+ ///
+ BottomLeft
+ }
+}
diff --git a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
index f020a2522..3ee9f761d 100644
--- a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
+++ b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
@@ -82,6 +82,32 @@ namespace ImageProcessorCore.Samplers
}
}
+ ///
+ /// Resizes an image in accordance with the given .
+ ///
+ /// The image to resize.
+ /// The resize options.
+ /// A delegate which is called as progress is made processing the image.
+ /// The
+ /// 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.
+ if (options.Size.Width == 0 && options.Size.Height > 0)
+ {
+ options.Size = new Size(source.Width * options.Size.Height / source.Height, options.Size.Height);
+ }
+
+ if (options.Size.Height == 0 && options.Size.Width > 0)
+ {
+ options.Size = new Size(options.Size.Width, source.Height * options.Size.Width / source.Width);
+ }
+
+ Rectangle targetRectangle = ResizeHelper.CalculateTargetLocationAndBounds(source, options);
+
+ return Resize(source, options.Size.Width, options.Size.Height, options.Sampler, source.Bounds, targetRectangle, options.Compand, progressHandler);
+ }
+
///
/// Resizes an image to the given width and height.
///
@@ -124,7 +150,7 @@ namespace ImageProcessorCore.Samplers
/// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image
public static Image Resize(this Image source, int width, int height, IResampler sampler, bool compand, ProgressEventHandler progressHandler = null)
{
- return Resize(source, width, height, sampler, source.Bounds, compand, progressHandler);
+ return Resize(source, width, height, sampler, source.Bounds, new Rectangle(0, 0, width, height), compand, progressHandler);
}
///
@@ -138,11 +164,14 @@ namespace ImageProcessorCore.Samplers
///
/// The structure that specifies the portion of the image object to draw.
///
+ ///
+ /// The structure that specifies the portion of the target image object to draw to.
+ ///
/// Whether to compress and expand the image color-space to gamma correct the image during processing.
/// A delegate which is called as progress is made processing the image.
/// The
/// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image
- public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, bool compand = false, ProgressEventHandler progressHandler = null)
+ public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, Rectangle targetRectangle, bool compand = false, ProgressEventHandler progressHandler = null)
{
if (width == 0 && height > 0)
{
@@ -159,7 +188,7 @@ namespace ImageProcessorCore.Samplers
try
{
- return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), processor);
+ return source.Process(width, height, sourceRectangle, targetRectangle, processor);
}
finally
{
diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs
index e7de40c44..92952ea7b 100644
--- a/src/ImageProcessorCore/Samplers/Resize.cs
+++ b/src/ImageProcessorCore/Samplers/Resize.cs
@@ -5,6 +5,7 @@
namespace ImageProcessorCore.Samplers
{
+ using System;
using System.Threading.Tasks;
///
@@ -47,12 +48,14 @@ namespace ImageProcessorCore.Samplers
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
// Jump out, we'll deal with that later.
- if (source.Bounds == target.Bounds)
+ // TODO: Add rectangle comparison.
+ if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
return;
}
- int sourceBottom = source.Bounds.Bottom;
+ int width = target.Width;
+ int height = target.Height;
int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X;
@@ -82,6 +85,7 @@ namespace ImageProcessorCore.Samplers
target[x, y] = source[originX, originY];
}
+
this.OnRowProcessed();
}
});
@@ -92,16 +96,20 @@ namespace ImageProcessorCore.Samplers
// Interpolate the image using the calculated weights.
// A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm
- // First process the columns.
+ // First process the columns. Since we are not using multiple threads startY and endY
+ // are the upper and lower bounds of the source rectangle.
Parallel.For(
- 0,
- sourceBottom,
+ startY,
+ endY,
y =>
{
for (int x = startX; x < endX; x++)
{
- float sum = this.HorizontalWeights[x].Sum;
- Weight[] horizontalValues = this.HorizontalWeights[x].Values;
+ // Ensure offsets are normalised for cropping and padding.
+ int offsetX = x - startX;
+
+ float sum = this.HorizontalWeights[offsetX].Sum;
+ Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components
Color destination = new Color();
@@ -119,7 +127,10 @@ namespace ImageProcessorCore.Samplers
destination = Color.Compress(destination);
}
- this.firstPass[x, y] = destination;
+ if (x >= 0 && x < width)
+ {
+ this.firstPass[x, y] = destination;
+ }
}
});
@@ -129,34 +140,37 @@ namespace ImageProcessorCore.Samplers
endY,
y =>
{
- if (y >= targetY && y < targetBottom)
+ // Ensure offsets are normalised for cropping and padding.
+ int offsetY = y - startY;
+
+ float sum = this.VerticalWeights[offsetY].Sum;
+ Weight[] verticalValues = this.VerticalWeights[offsetY].Values;
+
+ for (int x = 0; x < width; x++)
{
- float sum = this.VerticalWeights[y].Sum;
- Weight[] verticalValues = this.VerticalWeights[y].Values;
+ // Destination color components
+ Color destination = new Color();
- for (int x = startX; x < endX; x++)
+ for (int i = 0; i < sum; i++)
{
- // Destination color components
- Color destination = new Color();
-
- for (int i = 0; i < sum; i++)
- {
- Weight yw = verticalValues[i];
- int originY = yw.Index;
- Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
- destination += sourceColor * yw.Value;
- }
+ Weight yw = verticalValues[i];
+ int originY = yw.Index;
+ Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
+ destination += sourceColor * yw.Value;
+ }
- if (compand)
- {
- destination = Color.Compress(destination);
- }
+ if (compand)
+ {
+ destination = Color.Compress(destination);
+ }
+ if (y >= 0 && y < height)
+ {
target[x, y] = destination;
}
-
- this.OnRowProcessed();
}
+
+ this.OnRowProcessed();
});
}
@@ -164,7 +178,7 @@ namespace ImageProcessorCore.Samplers
protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{
// Copy the pixels over.
- if (source.Bounds == target.Bounds)
+ if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
target.ClonePixels(target.Width, target.Height, source.Pixels);
}
diff --git a/src/ImageProcessorCore/Samplers/ResizeHelper.cs b/src/ImageProcessorCore/Samplers/ResizeHelper.cs
new file mode 100644
index 000000000..dc72638b4
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/ResizeHelper.cs
@@ -0,0 +1,235 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Samplers
+{
+ using System;
+ using System.Linq;
+
+ ///
+ /// Provides methods to help calculate the target rectangle when resizing using the
+ /// enumeration.
+ ///
+ internal static class ResizeHelper
+ {
+ ///
+ /// Calculates the target location and bounds to perform the resize operation against.
+ ///
+ /// The source image.
+ /// The resize options.
+ ///
+ /// The .
+ ///
+ public static Rectangle CalculateTargetLocationAndBounds(ImageBase source, ResizeOptions options)
+ {
+ switch (options.Mode)
+ {
+ case ResizeMode.Pad:
+ return CalculatePadRectangle(source, options);
+
+ // TODO: Additional modes
+ // Default case ResizeMode.Crop
+ default:
+ return CalculateCropRectangle(source, options);
+ }
+ }
+
+ ///
+ /// Calculates the target rectangle for crop mode.
+ ///
+ /// The source image.
+ /// The resize options.
+ ///
+ /// The .
+ ///
+ private static Rectangle CalculateCropRectangle(ImageBase source, ResizeOptions options)
+ {
+ int width = options.Size.Width;
+ int height = options.Size.Height;
+
+ if (width <= 0 || height <= 0)
+ {
+ return new Rectangle(0, 0, source.Width, source.Height);
+ }
+
+ double ratio;
+ int sourceWidth = source.Width;
+ int sourceHeight = source.Height;
+
+ int destinationX = 0;
+ int destinationY = 0;
+ int destinationWidth = width;
+ int destinationHeight = height;
+
+ // Fractional variants for preserving aspect ratio.
+ double percentHeight = Math.Abs(height / (double)sourceHeight);
+ double percentWidth = Math.Abs(width / (double)sourceWidth);
+
+ if (percentHeight < percentWidth)
+ {
+ ratio = percentWidth;
+
+ if (options.CenterCoordinates.Any())
+ {
+ double center = -(ratio * sourceHeight) * options.CenterCoordinates.First();
+ destinationY = (int)center + (height / 2);
+
+ if (destinationY > 0)
+ {
+ destinationY = 0;
+ }
+
+ if (destinationY < (int)(height - (sourceHeight * ratio)))
+ {
+ destinationY = (int)(height - (sourceHeight * ratio));
+ }
+ }
+ else
+ {
+ switch (options.Position)
+ {
+ case AnchorPosition.Top:
+ case AnchorPosition.TopLeft:
+ case AnchorPosition.TopRight:
+ destinationY = 0;
+ break;
+ case AnchorPosition.Bottom:
+ case AnchorPosition.BottomLeft:
+ case AnchorPosition.BottomRight:
+ destinationY = (int)(height - (sourceHeight * ratio));
+ break;
+ default:
+ destinationY = (int)((height - (sourceHeight * ratio)) / 2);
+ break;
+ }
+ }
+
+ destinationHeight = (int)Math.Ceiling(sourceHeight * percentWidth);
+ }
+ else
+ {
+ ratio = percentHeight;
+
+ if (options.CenterCoordinates.Any())
+ {
+ double center = -(ratio * sourceWidth) * options.CenterCoordinates.ToArray()[1];
+ destinationX = (int)center + (width / 2);
+
+ if (destinationX > 0)
+ {
+ destinationX = 0;
+ }
+
+ if (destinationX < (int)(width - (sourceWidth * ratio)))
+ {
+ destinationX = (int)(width - (sourceWidth * ratio));
+ }
+ }
+ else
+ {
+ switch (options.Position)
+ {
+ case AnchorPosition.Left:
+ case AnchorPosition.TopLeft:
+ case AnchorPosition.BottomLeft:
+ destinationX = 0;
+ break;
+ case AnchorPosition.Right:
+ case AnchorPosition.TopRight:
+ case AnchorPosition.BottomRight:
+ destinationX = (int)(width - (sourceWidth * ratio));
+ break;
+ default:
+ destinationX = (int)((width - (sourceWidth * ratio)) / 2);
+ break;
+ }
+ }
+
+ destinationWidth = (int)Math.Ceiling(sourceWidth * percentHeight);
+ }
+
+ return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
+ }
+
+ ///
+ /// Calculates the target rectangle for pad mode.
+ ///
+ /// The source image.
+ /// The resize options.
+ ///
+ /// The .
+ ///
+ private static Rectangle CalculatePadRectangle(ImageBase source, ResizeOptions options)
+ {
+ int width = options.Size.Width;
+ int height = options.Size.Height;
+
+ if (width <= 0 || height <= 0)
+ {
+ return new Rectangle(0, 0, source.Width, source.Height);
+ }
+
+ double ratio;
+ int sourceWidth = source.Width;
+ int sourceHeight = source.Height;
+
+ int destinationX = 0;
+ int destinationY = 0;
+ int destinationWidth = width;
+ int destinationHeight = height;
+
+ // Fractional variants for preserving aspect ratio.
+ double percentHeight = Math.Abs(height / (double)sourceHeight);
+ double percentWidth = Math.Abs(width / (double)sourceWidth);
+
+ if (percentHeight < percentWidth)
+ {
+ ratio = percentHeight;
+ destinationWidth = Convert.ToInt32(sourceWidth * percentHeight);
+
+ switch (options.Position)
+ {
+ case AnchorPosition.Left:
+ case AnchorPosition.TopLeft:
+ case AnchorPosition.BottomLeft:
+ destinationX = 0;
+ break;
+ case AnchorPosition.Right:
+ case AnchorPosition.TopRight:
+ case AnchorPosition.BottomRight:
+ destinationX = (int)(width - (sourceWidth * ratio));
+ break;
+ default:
+ destinationX = Convert.ToInt32((width - (sourceWidth * ratio)) / 2);
+ break;
+ }
+ }
+ else
+ {
+ ratio = percentWidth;
+ destinationHeight = Convert.ToInt32(sourceHeight * percentWidth);
+
+ switch (options.Position)
+ {
+ case AnchorPosition.Top:
+ case AnchorPosition.TopLeft:
+ case AnchorPosition.TopRight:
+ destinationY = 0;
+ break;
+ case AnchorPosition.Bottom:
+ case AnchorPosition.BottomLeft:
+ case AnchorPosition.BottomRight:
+ destinationY = (int)(height - (sourceHeight * ratio));
+ break;
+ default:
+ destinationY = (int)((height - (sourceHeight * ratio)) / 2);
+ break;
+ }
+ }
+
+ return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
+ }
+ }
+}
diff --git a/src/ImageProcessorCore/Samplers/ResizeMode.cs b/src/ImageProcessorCore/Samplers/ResizeMode.cs
new file mode 100644
index 000000000..d5c30ca84
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/ResizeMode.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Samplers
+{
+ ///
+ /// Enumerated resize modes to apply to resized images.
+ ///
+ public enum ResizeMode
+ {
+ ///
+ /// Crops the resized image to fit the bounds of its container.
+ ///
+ Crop,
+
+ ///
+ /// Pads the resized image to fit the bounds of its container.
+ /// If only one dimension is passed, will maintain the original aspect ratio.
+ ///
+ Pad,
+
+ ///
+ /// Stretches the resized image to fit the bounds of its container.
+ ///
+ Stretch,
+
+ ///
+ /// Constrains the resized image to fit the bounds of its container maintaining
+ /// the original aspect ratio.
+ ///
+ Max,
+
+ ///
+ /// Resizes the image until the shortest side reaches the set given dimension.
+ ///
+ Min,
+
+ ///
+ /// Pads the image to fit the bound of the container without resizing the
+ /// original source.
+ /// When downscaling, performs the same functionality as
+ ///
+ BoxPad
+ }
+}
diff --git a/src/ImageProcessorCore/Samplers/ResizeOptions.cs b/src/ImageProcessorCore/Samplers/ResizeOptions.cs
new file mode 100644
index 000000000..66f262c63
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/ResizeOptions.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Samplers
+{
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// The resize options for resizing images against certain modes.
+ ///
+ public class ResizeOptions
+ {
+ ///
+ /// Gets or sets the resize mode.
+ ///
+ public ResizeMode Mode { get; set; } = ResizeMode.Crop;
+
+ ///
+ /// Gets or sets the anchor position.
+ ///
+ public AnchorPosition Position { get; set; } = AnchorPosition.Center;
+
+ ///
+ /// Gets or sets the center coordinates.
+ ///
+ public IEnumerable CenterCoordinates { get; set; } = Enumerable.Empty();
+
+ ///
+ /// Gets or sets the target size.
+ ///
+ public Size Size { get; set; }
+
+ public IResampler Sampler { get; set; } = new BicubicResampler();
+
+ public bool Compand { get; set; }
+ }
+}
diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs
index 1b0a0cf55..99fedb33b 100644
--- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs
+++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs
@@ -141,7 +141,7 @@
Directory.CreateDirectory("TestOutput/Resize");
}
- var name = "FixedHeight";
+ string name = "FixedHeight";
foreach (string file in Files)
{
@@ -162,6 +162,73 @@
}
}
+ [Fact]
+ public void ImageShouldResizeWithCropMode()
+ {
+ if (!Directory.Exists("TestOutput/ResizeCrop"))
+ {
+ Directory.CreateDirectory("TestOutput/ResizeCrop");
+ }
+
+ 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/ResizeCrop/{filename}"))
+ {
+ ResizeOptions options = new ResizeOptions()
+ {
+ Size = new Size(image.Width / 2, image.Height)
+ };
+
+ image.Resize(options, this.ProgressUpdate)
+ .Save(output);
+ }
+
+ Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
+ }
+ }
+
+ }
+
+ [Fact]
+ public void ImageShouldResizeWithPadMode()
+ {
+ if (!Directory.Exists("TestOutput/ResizePad"))
+ {
+ Directory.CreateDirectory("TestOutput/ResizePad");
+ }
+
+ 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/ResizePad/{filename}"))
+ {
+ ResizeOptions options = new ResizeOptions()
+ {
+ Size = new Size(image.Width + 200, image.Height),
+ Mode = ResizeMode.Pad
+ };
+
+ image.Resize(options, this.ProgressUpdate)
+ .Save(output);
+ }
+
+ Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
+ }
+ }
+
+ }
+
[Theory]
[MemberData("RotateFlips")]
public void ImageShouldRotateFlip(RotateType rotateType, FlipType flipType)
@@ -281,7 +348,7 @@
{
using (FileStream stream = File.OpenRead(file))
{
-
+
string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (Image image = new Image(stream))