Browse Source

Merge pull request #1003 from SixLabors/issue/999

Fix #999 and add tests
af/merge-core
Anton Firsov 7 years ago
committed by GitHub
parent
commit
9eb03a49a9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      src/ImageSharp/Processing/Extensions/ResizeExtensions.cs
  2. 257
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs
  3. 82
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
  4. 14
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  5. 9
      src/ImageSharp/Processing/ResizeMode.cs
  6. 13
      src/ImageSharp/Processing/ResizeOptions.cs
  7. 143
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs
  8. 4
      tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs
  9. 20
      tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs
  10. 8
      tests/ImageSharp.Tests/xunit.runner.json

50
src/ImageSharp/Processing/Extensions/ResizeExtensions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -12,16 +12,6 @@ namespace SixLabors.ImageSharp.Processing
/// </summary> /// </summary>
public static class ResizeExtensions public static class ResizeExtensions
{ {
/// <summary>
/// Resizes an image in accordance with the given <see cref="ResizeOptions"/>.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="options">The resize options.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
/// <remarks>Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image or the nearest possible ratio.</remarks>
public static IImageProcessingContext Resize(this IImageProcessingContext source, ResizeOptions options)
=> source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize()));
/// <summary> /// <summary>
/// Resizes an image to the given <see cref="Size"/>. /// Resizes an image to the given <see cref="Size"/>.
/// </summary> /// </summary>
@ -128,7 +118,18 @@ namespace SixLabors.ImageSharp.Processing
Rectangle sourceRectangle, Rectangle sourceRectangle,
Rectangle targetRectangle, Rectangle targetRectangle,
bool compand) bool compand)
=> source.ApplyProcessor(new ResizeProcessor(sampler, width, height, source.GetCurrentSize(), targetRectangle, compand), sourceRectangle); {
var options = new ResizeOptions
{
Size = new Size(width, height),
Mode = ResizeMode.Manual,
Sampler = sampler,
TargetRectangle = targetRectangle,
Compand = compand
};
return source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize()), sourceRectangle);
}
/// <summary> /// <summary>
/// Resizes an image to the given width and height with the given sampler and source rectangle. /// Resizes an image to the given width and height with the given sampler and source rectangle.
@ -150,6 +151,27 @@ namespace SixLabors.ImageSharp.Processing
IResampler sampler, IResampler sampler,
Rectangle targetRectangle, Rectangle targetRectangle,
bool compand) bool compand)
=> source.ApplyProcessor(new ResizeProcessor(sampler, width, height, source.GetCurrentSize(), targetRectangle, compand)); {
var options = new ResizeOptions
{
Size = new Size(width, height),
Mode = ResizeMode.Manual,
Sampler = sampler,
TargetRectangle = targetRectangle,
Compand = compand
};
return Resize(source, options);
}
/// <summary>
/// Resizes an image in accordance with the given <see cref="ResizeOptions"/>.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="options">The resize options.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
/// <remarks>Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image or the nearest possible ratio.</remarks>
public static IImageProcessingContext Resize(this IImageProcessingContext source, ResizeOptions options)
=> source.ApplyProcessor(new ResizeProcessor(options, source.GetCurrentSize()));
} }
} }

257
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs

@ -2,9 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Linq;
using System.Numerics; using System.Numerics;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -30,17 +28,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary> /// </summary>
/// <param name="sourceSize">The source image size.</param> /// <param name="sourceSize">The source image size.</param>
/// <param name="options">The resize options.</param> /// <param name="options">The resize options.</param>
/// <param name="width">The target width</param>
/// <param name="height">The target height</param>
/// <returns> /// <returns>
/// The tuple representing the location and the bounds /// The tuple representing the location and the bounds
/// </returns> /// </returns>
public static (Size, Rectangle) CalculateTargetLocationAndBounds( public static (Size, Rectangle) CalculateTargetLocationAndBounds(Size sourceSize, ResizeOptions options)
Size sourceSize,
ResizeOptions options,
int width,
int height)
{ {
int width = options.Size.Width;
int height = options.Size.Height;
if (width <= 0 && height <= 0)
{
ThrowInvalid($"Target width {width} and height {height} must be greater than zero.");
}
// Ensure target size is populated across both dimensions.
// These dimensions are used to calculate the final dimensions determined by the mode algorithm.
// If only one of the incoming dimensions is 0, it will be modified here to maintain aspect ratio.
// If it is not possible to keep aspect ratio, make sure at least the minimum is is kept.
const int Min = 1;
if (width == 0 && height > 0)
{
width = (int)MathF.Max(Min, MathF.Round(sourceSize.Width * height / (float)sourceSize.Height));
}
if (height == 0 && width > 0)
{
height = (int)MathF.Max(Min, MathF.Round(sourceSize.Height * width / (float)sourceSize.Width));
}
switch (options.Mode) switch (options.Mode)
{ {
case ResizeMode.Crop: case ResizeMode.Crop:
@ -50,11 +65,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
case ResizeMode.BoxPad: case ResizeMode.BoxPad:
return CalculateBoxPadRectangle(sourceSize, options, width, height); return CalculateBoxPadRectangle(sourceSize, options, width, height);
case ResizeMode.Max: case ResizeMode.Max:
return CalculateMaxRectangle(sourceSize, options, width, height); return CalculateMaxRectangle(sourceSize, width, height);
case ResizeMode.Min: case ResizeMode.Min:
return CalculateMinRectangle(sourceSize, options, width, height); return CalculateMinRectangle(sourceSize, width, height);
case ResizeMode.Manual:
return CalculateManualRectangle(options, width, height);
// Last case ResizeMode.Stretch: // case ResizeMode.Stretch:
default: default:
return (new Size(width, height), new Rectangle(0, 0, width, height)); return (new Size(width, height), new Rectangle(0, 0, width, height));
} }
@ -66,11 +83,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width, int width,
int height) int height)
{ {
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
int sourceWidth = source.Width; int sourceWidth = source.Width;
int sourceHeight = source.Height; int sourceHeight = source.Height;
@ -84,55 +96,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
// Only calculate if upscaling. // Only calculate if upscaling.
if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight)
{ {
int destinationX; int targetX;
int destinationY; int targetY;
int destinationWidth = sourceWidth; int targetWidth = sourceWidth;
int destinationHeight = sourceHeight; int targetHeight = sourceHeight;
width = boxPadWidth; width = boxPadWidth;
height = boxPadHeight; height = boxPadHeight;
switch (options.Position) switch (options.Position)
{ {
case AnchorPositionMode.Left: case AnchorPositionMode.Left:
destinationY = (height - sourceHeight) / 2; targetY = (height - sourceHeight) / 2;
destinationX = 0; targetX = 0;
break; break;
case AnchorPositionMode.Right: case AnchorPositionMode.Right:
destinationY = (height - sourceHeight) / 2; targetY = (height - sourceHeight) / 2;
destinationX = width - sourceWidth; targetX = width - sourceWidth;
break; break;
case AnchorPositionMode.TopRight: case AnchorPositionMode.TopRight:
destinationY = 0; targetY = 0;
destinationX = width - sourceWidth; targetX = width - sourceWidth;
break; break;
case AnchorPositionMode.Top: case AnchorPositionMode.Top:
destinationY = 0; targetY = 0;
destinationX = (width - sourceWidth) / 2; targetX = (width - sourceWidth) / 2;
break; break;
case AnchorPositionMode.TopLeft: case AnchorPositionMode.TopLeft:
destinationY = 0; targetY = 0;
destinationX = 0; targetX = 0;
break; break;
case AnchorPositionMode.BottomRight: case AnchorPositionMode.BottomRight:
destinationY = height - sourceHeight; targetY = height - sourceHeight;
destinationX = width - sourceWidth; targetX = width - sourceWidth;
break; break;
case AnchorPositionMode.Bottom: case AnchorPositionMode.Bottom:
destinationY = height - sourceHeight; targetY = height - sourceHeight;
destinationX = (width - sourceWidth) / 2; targetX = (width - sourceWidth) / 2;
break; break;
case AnchorPositionMode.BottomLeft: case AnchorPositionMode.BottomLeft:
destinationY = height - sourceHeight; targetY = height - sourceHeight;
destinationX = 0; targetX = 0;
break; break;
default: default:
destinationY = (height - sourceHeight) / 2; targetY = (height - sourceHeight) / 2;
destinationX = (width - sourceWidth) / 2; targetX = (width - sourceWidth) / 2;
break; break;
} }
return (new Size(width, height), // Target image width and height can be different to the rectangle width and height.
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
} }
// Switch to pad mode to downscale and calculate from there. // Switch to pad mode to downscale and calculate from there.
@ -145,19 +157,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width, int width,
int height) int height)
{ {
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
float ratio; float ratio;
int sourceWidth = source.Width; int sourceWidth = source.Width;
int sourceHeight = source.Height; int sourceHeight = source.Height;
int destinationX = 0; int targetX = 0;
int destinationY = 0; int targetY = 0;
int destinationWidth = width; int targetWidth = width;
int destinationHeight = height; int targetHeight = height;
// Fractional variants for preserving aspect ratio. // Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight); float percentHeight = MathF.Abs(height / (float)sourceHeight);
@ -167,19 +174,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
ratio = percentWidth; ratio = percentWidth;
if (options.CenterCoordinates.Any()) if (options.CenterCoordinates.HasValue)
{ {
float center = -(ratio * sourceHeight) * options.CenterCoordinates.ToArray()[1]; float center = -(ratio * sourceHeight) * options.CenterCoordinates.Value.Y;
destinationY = (int)MathF.Round(center + (height / 2F)); targetY = (int)MathF.Round(center + (height / 2F));
if (destinationY > 0) if (targetY > 0)
{ {
destinationY = 0; targetY = 0;
} }
if (destinationY < (int)MathF.Round(height - (sourceHeight * ratio))) if (targetY < (int)MathF.Round(height - (sourceHeight * ratio)))
{ {
destinationY = (int)MathF.Round(height - (sourceHeight * ratio)); targetY = (int)MathF.Round(height - (sourceHeight * ratio));
} }
} }
else else
@ -189,38 +196,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
case AnchorPositionMode.Top: case AnchorPositionMode.Top:
case AnchorPositionMode.TopLeft: case AnchorPositionMode.TopLeft:
case AnchorPositionMode.TopRight: case AnchorPositionMode.TopRight:
destinationY = 0; targetY = 0;
break; break;
case AnchorPositionMode.Bottom: case AnchorPositionMode.Bottom:
case AnchorPositionMode.BottomLeft: case AnchorPositionMode.BottomLeft:
case AnchorPositionMode.BottomRight: case AnchorPositionMode.BottomRight:
destinationY = (int)MathF.Round(height - (sourceHeight * ratio)); targetY = (int)MathF.Round(height - (sourceHeight * ratio));
break; break;
default: default:
destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); targetY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F);
break; break;
} }
} }
destinationHeight = (int)MathF.Ceiling(sourceHeight * percentWidth); targetHeight = (int)MathF.Ceiling(sourceHeight * percentWidth);
} }
else else
{ {
ratio = percentHeight; ratio = percentHeight;
if (options.CenterCoordinates.Any()) if (options.CenterCoordinates.HasValue)
{ {
float center = -(ratio * sourceWidth) * options.CenterCoordinates.First(); float center = -(ratio * sourceWidth) * options.CenterCoordinates.Value.X;
destinationX = (int)MathF.Round(center + (width / 2F)); targetX = (int)MathF.Round(center + (width / 2F));
if (destinationX > 0) if (targetX > 0)
{ {
destinationX = 0; targetX = 0;
} }
if (destinationX < (int)MathF.Round(width - (sourceWidth * ratio))) if (targetX < (int)MathF.Round(width - (sourceWidth * ratio)))
{ {
destinationX = (int)MathF.Round(width - (sourceWidth * ratio)); targetX = (int)MathF.Round(width - (sourceWidth * ratio));
} }
} }
else else
@ -230,68 +237,64 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
case AnchorPositionMode.Left: case AnchorPositionMode.Left:
case AnchorPositionMode.TopLeft: case AnchorPositionMode.TopLeft:
case AnchorPositionMode.BottomLeft: case AnchorPositionMode.BottomLeft:
destinationX = 0; targetX = 0;
break; break;
case AnchorPositionMode.Right: case AnchorPositionMode.Right:
case AnchorPositionMode.TopRight: case AnchorPositionMode.TopRight:
case AnchorPositionMode.BottomRight: case AnchorPositionMode.BottomRight:
destinationX = (int)MathF.Round(width - (sourceWidth * ratio)); targetX = (int)MathF.Round(width - (sourceWidth * ratio));
break; break;
default: default:
destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); targetX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F);
break; break;
} }
} }
destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight); targetWidth = (int)MathF.Ceiling(sourceWidth * percentHeight);
} }
return (new Size(width, height), // Target image width and height can be different to the rectangle width and height.
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
} }
private static (Size, Rectangle) CalculateMaxRectangle( private static (Size, Rectangle) CalculateMaxRectangle(
Size source, Size source,
ResizeOptions options,
int width, int width,
int height) int height)
{ {
int destinationWidth = width; int targetWidth = width;
int destinationHeight = height; int targetHeight = height;
// Fractional variants for preserving aspect ratio. // Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)source.Height); float percentHeight = MathF.Abs(height / (float)source.Height);
float percentWidth = MathF.Abs(width / (float)source.Width); float percentWidth = MathF.Abs(width / (float)source.Width);
// Integers must be cast to floats to get needed precision // Integers must be cast to floats to get needed precision
float ratio = options.Size.Height / (float)options.Size.Width; float ratio = height / (float)width;
float sourceRatio = source.Height / (float)source.Width; float sourceRatio = source.Height / (float)source.Width;
if (sourceRatio < ratio) if (sourceRatio < ratio)
{ {
destinationHeight = (int)MathF.Round(source.Height * percentWidth); targetHeight = (int)MathF.Round(source.Height * percentWidth);
height = destinationHeight;
} }
else else
{ {
destinationWidth = (int)MathF.Round(source.Width * percentHeight); targetWidth = (int)MathF.Round(source.Width * percentHeight);
width = destinationWidth;
} }
// Replace the size to match the rectangle. // Replace the size to match the rectangle.
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); return (new Size(targetWidth, targetHeight), new Rectangle(0, 0, targetWidth, targetHeight));
} }
private static (Size, Rectangle) CalculateMinRectangle( private static (Size, Rectangle) CalculateMinRectangle(
Size source, Size source,
ResizeOptions options,
int width, int width,
int height) int height)
{ {
int sourceWidth = source.Width; int sourceWidth = source.Width;
int sourceHeight = source.Height; int sourceHeight = source.Height;
int destinationWidth; int targetWidth = width;
int destinationHeight; int targetHeight = height;
// Don't upscale // Don't upscale
if (width > sourceWidth || height > sourceHeight) if (width > sourceWidth || height > sourceHeight)
@ -306,58 +309,45 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (widthDiff < heightDiff) if (widthDiff < heightDiff)
{ {
float sourceRatio = (float)sourceHeight / sourceWidth; float sourceRatio = (float)sourceHeight / sourceWidth;
destinationHeight = (int)MathF.Round(width * sourceRatio); targetHeight = (int)MathF.Round(width * sourceRatio);
height = destinationHeight;
destinationWidth = width;
} }
else if (widthDiff > heightDiff) else if (widthDiff > heightDiff)
{ {
float sourceRatioInverse = (float)sourceWidth / sourceHeight; float sourceRatioInverse = (float)sourceWidth / sourceHeight;
destinationWidth = (int)MathF.Round(height * sourceRatioInverse); targetWidth = (int)MathF.Round(height * sourceRatioInverse);
destinationHeight = height;
width = destinationWidth;
} }
else else
{ {
if (height > width) if (height > width)
{ {
destinationWidth = width;
float percentWidth = MathF.Abs(width / (float)sourceWidth); float percentWidth = MathF.Abs(width / (float)sourceWidth);
destinationHeight = (int)MathF.Round(sourceHeight * percentWidth); targetHeight = (int)MathF.Round(sourceHeight * percentWidth);
height = destinationHeight;
} }
else else
{ {
destinationHeight = height;
float percentHeight = MathF.Abs(height / (float)sourceHeight); float percentHeight = MathF.Abs(height / (float)sourceHeight);
destinationWidth = (int)MathF.Round(sourceWidth * percentHeight); targetWidth = (int)MathF.Round(sourceWidth * percentHeight);
width = destinationWidth;
} }
} }
// Replace the size to match the rectangle. // Replace the size to match the rectangle.
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); return (new Size(targetWidth, targetHeight), new Rectangle(0, 0, targetWidth, targetHeight));
} }
private static (Size, Rectangle) CalculatePadRectangle( private static (Size, Rectangle) CalculatePadRectangle(
Size source, Size sourceSize,
ResizeOptions options, ResizeOptions options,
int width, int width,
int height) int height)
{ {
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
float ratio; float ratio;
int sourceWidth = source.Width; int sourceWidth = sourceSize.Width;
int sourceHeight = source.Height; int sourceHeight = sourceSize.Height;
int destinationX = 0; int targetX = 0;
int destinationY = 0; int targetY = 0;
int destinationWidth = width; int targetWidth = width;
int destinationHeight = height; int targetHeight = height;
// Fractional variants for preserving aspect ratio. // Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight); float percentHeight = MathF.Abs(height / (float)sourceHeight);
@ -366,50 +356,73 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (percentHeight < percentWidth) if (percentHeight < percentWidth)
{ {
ratio = percentHeight; ratio = percentHeight;
destinationWidth = (int)MathF.Round(sourceWidth * percentHeight); targetWidth = (int)MathF.Round(sourceWidth * percentHeight);
switch (options.Position) switch (options.Position)
{ {
case AnchorPositionMode.Left: case AnchorPositionMode.Left:
case AnchorPositionMode.TopLeft: case AnchorPositionMode.TopLeft:
case AnchorPositionMode.BottomLeft: case AnchorPositionMode.BottomLeft:
destinationX = 0; targetX = 0;
break; break;
case AnchorPositionMode.Right: case AnchorPositionMode.Right:
case AnchorPositionMode.TopRight: case AnchorPositionMode.TopRight:
case AnchorPositionMode.BottomRight: case AnchorPositionMode.BottomRight:
destinationX = (int)MathF.Round(width - (sourceWidth * ratio)); targetX = (int)MathF.Round(width - (sourceWidth * ratio));
break; break;
default: default:
destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); targetX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F);
break; break;
} }
} }
else else
{ {
ratio = percentWidth; ratio = percentWidth;
destinationHeight = (int)MathF.Round(sourceHeight * percentWidth); targetHeight = (int)MathF.Round(sourceHeight * percentWidth);
switch (options.Position) switch (options.Position)
{ {
case AnchorPositionMode.Top: case AnchorPositionMode.Top:
case AnchorPositionMode.TopLeft: case AnchorPositionMode.TopLeft:
case AnchorPositionMode.TopRight: case AnchorPositionMode.TopRight:
destinationY = 0; targetY = 0;
break; break;
case AnchorPositionMode.Bottom: case AnchorPositionMode.Bottom:
case AnchorPositionMode.BottomLeft: case AnchorPositionMode.BottomLeft:
case AnchorPositionMode.BottomRight: case AnchorPositionMode.BottomRight:
destinationY = (int)MathF.Round(height - (sourceHeight * ratio)); targetY = (int)MathF.Round(height - (sourceHeight * ratio));
break; break;
default: default:
destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); targetY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F);
break; break;
} }
} }
return (new Size(width, height), // Target image width and height can be different to the rectangle width and height.
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
} }
private static (Size, Rectangle) CalculateManualRectangle(
ResizeOptions options,
int width,
int height)
{
if (!options.TargetRectangle.HasValue)
{
ThrowInvalid("Manual resizing requires a target location and size.");
}
Rectangle targetRectangle = options.TargetRectangle.Value;
int targetX = targetRectangle.X;
int targetY = targetRectangle.Y;
int targetWidth = targetRectangle.Width > 0 ? targetRectangle.Width : width;
int targetHeight = targetRectangle.Height > 0 ? targetRectangle.Height : height;
// Target image width and height can be different to the rectangle width and height.
return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
}
private static void ThrowInvalid(string message) => throw new InvalidOperationException(message);
} }
} }

82
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs

@ -13,45 +13,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary> /// </summary>
public class ResizeProcessor : IImageProcessor public class ResizeProcessor : IImageProcessor
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor"/> class.
/// </summary>
/// <param name="sampler">The <see cref="IResampler"/>.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="sourceSize">The size of the source image.</param>
/// <param name="targetRectangle">The target rectangle to resize into.</param>
/// <param name="compand">A value indicating whether to apply RGBA companding.</param>
public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle targetRectangle, bool compand)
{
Guard.NotNull(sampler, nameof(sampler));
// Ensure size is populated across both dimensions.
// If only one of the incoming dimensions is 0, it will be modified here to maintain aspect ratio.
// If it is not possible to keep aspect ratio, make sure at least the minimum is is kept.
const int min = 1;
if (width == 0 && height > 0)
{
width = (int)MathF.Max(min, MathF.Round(sourceSize.Width * height / (float)sourceSize.Height));
targetRectangle.Width = width;
}
if (height == 0 && width > 0)
{
height = (int)MathF.Max(min, MathF.Round(sourceSize.Height * width / (float)sourceSize.Width));
targetRectangle.Height = height;
}
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.Sampler = sampler;
this.Width = width;
this.Height = height;
this.TargetRectangle = targetRectangle;
this.Compand = compand;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor"/> class. /// Initializes a new instance of the <see cref="ResizeProcessor"/> class.
/// </summary> /// </summary>
@ -62,48 +23,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(options.Sampler, nameof(options.Sampler)); Guard.NotNull(options.Sampler, nameof(options.Sampler));
int targetWidth = options.Size.Width; (Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options);
int targetHeight = options.Size.Height;
// Ensure size is populated across both dimensions.
// These dimensions are used to calculate the final dimensions determined by the mode algorithm.
// If only one of the incoming dimensions is 0, it will be modified here to maintain aspect ratio.
// If it is not possible to keep aspect ratio, make sure at least the minimum is is kept.
const int min = 1;
if (targetWidth == 0 && targetHeight > 0)
{
targetWidth = (int)MathF.Max(min, MathF.Round(sourceSize.Width * targetHeight / (float)sourceSize.Height));
}
if (targetHeight == 0 && targetWidth > 0)
{
targetHeight = (int)MathF.Max(min, MathF.Round(sourceSize.Height * targetWidth / (float)sourceSize.Width));
}
Guard.MustBeGreaterThan(targetWidth, 0, nameof(targetWidth));
Guard.MustBeGreaterThan(targetHeight, 0, nameof(targetHeight));
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options, targetWidth, targetHeight);
this.Sampler = options.Sampler; this.Sampler = options.Sampler;
this.Width = size.Width; this.TargetWidth = size.Width;
this.Height = size.Height; this.TargetHeight = size.Height;
this.TargetRectangle = rectangle; this.TargetRectangle = rectangle;
this.Compand = options.Compand; this.Compand = options.Compand;
} }
/// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor"/> class.
/// </summary>
/// <param name="sampler">The sampler to perform the resize operation.</param>
/// <param name="width">The target width.</param>
/// <param name="height">The target height.</param>
/// <param name="sourceSize">The source image size</param>
public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize)
: this(sampler, width, height, sourceSize, new Rectangle(0, 0, width, height), false)
{
}
/// <summary> /// <summary>
/// Gets the sampler to perform the resize operation. /// Gets the sampler to perform the resize operation.
/// </summary> /// </summary>
@ -112,12 +40,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary> /// <summary>
/// Gets the target width. /// Gets the target width.
/// </summary> /// </summary>
public int Width { get; } public int TargetWidth { get; }
/// <summary> /// <summary>
/// Gets the target height. /// Gets the target height.
/// </summary> /// </summary>
public int Height { get; } public int TargetHeight { get; }
/// <summary> /// <summary>
/// Gets the resize rectangle. /// Gets the resize rectangle.

14
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs

@ -45,15 +45,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary> /// <summary>
/// Gets the target width. /// Gets the target width.
/// </summary> /// </summary>
public int Width => this.parameterSource.Width; public int TargetWidth => this.parameterSource.TargetWidth;
/// <summary> /// <summary>
/// Gets the target height. /// Gets the target height.
/// </summary> /// </summary>
public int Height => this.parameterSource.Height; public int TargetHeight => this.parameterSource.TargetHeight;
/// <summary> /// <summary>
/// Gets the resize rectangle. /// Gets the target resize rectangle.
/// </summary> /// </summary>
public Rectangle TargetRectangle => this.parameterSource.TargetRectangle; public Rectangle TargetRectangle => this.parameterSource.TargetRectangle;
@ -80,8 +80,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel>>( IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel>>(
x => new ImageFrame<TPixel>( x => new ImageFrame<TPixel>(
configuration, configuration,
this.Width, this.TargetWidth,
this.Height, this.TargetHeight,
x.Metadata.DeepClone())); x.Metadata.DeepClone()));
// Use the overload to prevent an extra frame being added // Use the overload to prevent an extra frame being added
@ -128,8 +128,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return; return;
} }
int width = this.Width; int width = this.TargetWidth;
int height = this.Height; int height = this.TargetHeight;
int sourceX = sourceRectangle.X; int sourceX = sourceRectangle.X;
int sourceY = sourceRectangle.Y; int sourceY = sourceRectangle.Y;
int startY = this.TargetRectangle.Y; int startY = this.TargetRectangle.Y;

9
src/ImageSharp/Processing/ResizeMode.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing namespace SixLabors.ImageSharp.Processing
@ -42,6 +42,11 @@ namespace SixLabors.ImageSharp.Processing
/// <summary> /// <summary>
/// Stretches the resized image to fit the bounds of its container. /// Stretches the resized image to fit the bounds of its container.
/// </summary> /// </summary>
Stretch Stretch,
/// <summary>
/// The target location and size of the resized image has been manually set.
/// </summary>
Manual
} }
} }

13
src/ImageSharp/Processing/ResizeOptions.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -26,7 +24,7 @@ namespace SixLabors.ImageSharp.Processing
/// <summary> /// <summary>
/// Gets or sets the center coordinates. /// Gets or sets the center coordinates.
/// </summary> /// </summary>
public IEnumerable<float> CenterCoordinates { get; set; } = Array.Empty<float>(); public PointF? CenterCoordinates { get; set; }
/// <summary> /// <summary>
/// Gets or sets the target size. /// Gets or sets the target size.
@ -43,5 +41,10 @@ namespace SixLabors.ImageSharp.Processing
/// or expand individual pixel colors the value on processing. /// or expand individual pixel colors the value on processing.
/// </summary> /// </summary>
public bool Compand { get; set; } = false; public bool Compand { get; set; } = false;
/// <summary>
/// Gets or sets the target rectangle to resize into.
/// </summary>
public Rectangle? TargetRectangle { get; set; }
} }
} }

143
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs

@ -11,19 +11,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
public class ResizeHelperTests public class ResizeHelperTests
{ {
[Theory] [Theory]
[InlineData(20, 100, 1, 2)] [InlineData(20, 100, 1, 2)]
[InlineData(20, 100, 20*100*16, 2)] [InlineData(20, 100, 20 * 100 * 16, 2)]
[InlineData(20, 100, 40*100*16, 2)] [InlineData(20, 100, 40 * 100 * 16, 2)]
[InlineData(20, 100, 59*100*16, 2)] [InlineData(20, 100, 59 * 100 * 16, 2)]
[InlineData(20, 100, 60*100*16, 3)] [InlineData(20, 100, 60 * 100 * 16, 3)]
[InlineData(17, 63, 5*17*63*16, 5)] [InlineData(17, 63, 5 * 17 * 63 * 16, 5)]
[InlineData(17, 63, 5*17*63*16+1, 5)] [InlineData(17, 63, (5 * 17 * 63 * 16) + 1, 5)]
[InlineData(17, 63, 6*17*63*16-1, 5)] [InlineData(17, 63, (6 * 17 * 63 * 16) - 1, 5)]
[InlineData(33, 400, 1*1024*1024, 4)] [InlineData(33, 400, 1 * 1024 * 1024, 4)]
[InlineData(33, 400, 8*1024*1024, 39)] [InlineData(33, 400, 8 * 1024 * 1024, 39)]
[InlineData(50, 300, 1*1024*1024, 4)] [InlineData(50, 300, 1 * 1024 * 1024, 4)]
public void CalculateResizeWorkerHeightInWindowBands( public void CalculateResizeWorkerHeightInWindowBands(
int windowDiameter, int windowDiameter,
int width, int width,
@ -40,17 +39,121 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
var sourceSize = new Size(200, 100); var sourceSize = new Size(200, 100);
var target = new Size(400, 200); var target = new Size(400, 200);
var actual = ResizeHelper.CalculateTargetLocationAndBounds( (Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize, sourceSize,
new ResizeOptions{ new ResizeOptions
{
Mode = ResizeMode.Min, Mode = ResizeMode.Min,
Size = target Size = target
}, });
target.Width,
target.Height); Assert.Equal(sourceSize, size);
Assert.Equal(new Rectangle(0, 0, sourceSize.Width, sourceSize.Height), rectangle);
Assert.Equal(sourceSize, actual.Item1); }
Assert.Equal(new Rectangle(0, 0, sourceSize.Width, sourceSize.Height), actual.Item2);
[Fact]
public void MaxSizeAndRectangleAreCorrect()
{
var sourceSize = new Size(5072, 6761);
var target = new Size(0, 450);
var expectedSize = new Size(338, 450);
var expectedRectangle = new Rectangle(Point.Empty, expectedSize);
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize,
new ResizeOptions
{
Mode = ResizeMode.Max,
Size = target
});
Assert.Equal(expectedSize, size);
Assert.Equal(expectedRectangle, rectangle);
}
[Fact]
public void CropSizeAndRectangleAreCorrect()
{
var sourceSize = new Size(100, 100);
var target = new Size(25, 50);
var expectedSize = new Size(25, 50);
var expectedRectangle = new Rectangle(-12, 0, 50, 50);
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize,
new ResizeOptions
{
Mode = ResizeMode.Crop,
Size = target
});
Assert.Equal(expectedSize, size);
Assert.Equal(expectedRectangle, rectangle);
}
[Fact]
public void BoxPadSizeAndRectangleAreCorrect()
{
var sourceSize = new Size(100, 100);
var target = new Size(120, 110);
var expectedSize = new Size(120, 110);
var expectedRectangle = new Rectangle(10, 5, 100, 100);
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize,
new ResizeOptions
{
Mode = ResizeMode.BoxPad,
Size = target
});
Assert.Equal(expectedSize, size);
Assert.Equal(expectedRectangle, rectangle);
}
[Fact]
public void PadSizeAndRectangleAreCorrect()
{
var sourceSize = new Size(100, 100);
var target = new Size(120, 110);
var expectedSize = new Size(120, 110);
var expectedRectangle = new Rectangle(5, 0, 110, 110);
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize,
new ResizeOptions
{
Mode = ResizeMode.Pad,
Size = target
});
Assert.Equal(expectedSize, size);
Assert.Equal(expectedRectangle, rectangle);
}
[Fact]
public void StretchSizeAndRectangleAreCorrect()
{
var sourceSize = new Size(100, 100);
var target = new Size(57, 32);
var expectedSize = new Size(57, 32);
var expectedRectangle = new Rectangle(Point.Empty, expectedSize);
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(
sourceSize,
new ResizeOptions
{
Mode = ResizeMode.Stretch,
Size = target
});
Assert.Equal(expectedSize, size);
Assert.Equal(expectedRectangle, rectangle);
} }
} }
} }

4
tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs

@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Pad(width, height); this.operations.Pad(width, height);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>(); ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.Width); Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.Height); Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(sampler, resizeProcessor.Sampler); Assert.Equal(sampler, resizeProcessor.Sampler);
} }
} }

20
tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@ -18,8 +18,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height); this.operations.Resize(width, height);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>(); ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.Width); Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.Height); Assert.Equal(height, resizeProcessor.TargetHeight);
} }
[Fact] [Fact]
@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height, sampler); this.operations.Resize(width, height, sampler);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>(); ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.Width); Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.Height); Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(sampler, resizeProcessor.Sampler); Assert.Equal(sampler, resizeProcessor.Sampler);
} }
@ -48,8 +48,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height, sampler, compand); this.operations.Resize(width, height, sampler, compand);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>(); ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.Width); Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.Height); Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(sampler, resizeProcessor.Sampler); Assert.Equal(sampler, resizeProcessor.Sampler);
Assert.Equal(compand, resizeProcessor.Compand); Assert.Equal(compand, resizeProcessor.Compand);
} }
@ -74,8 +74,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(resizeOptions); this.operations.Resize(resizeOptions);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>(); ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.Width); Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.Height); Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(sampler, resizeProcessor.Sampler); Assert.Equal(sampler, resizeProcessor.Sampler);
Assert.Equal(compand, resizeProcessor.Compand); Assert.Equal(compand, resizeProcessor.Compand);
@ -87,4 +87,4 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
Assert.Equal(mode, resizeOptions.Mode); Assert.Equal(mode, resizeOptions.Mode);
} }
} }
} }

8
tests/ImageSharp.Tests/xunit.runner.json

@ -1,5 +1,5 @@
{ {
"shadowCopy": false, "shadowCopy": false,
"methodDisplay": "method", "methodDisplay": "method",
"diagnosticMessages": true "diagnosticMessages": true
} }

Loading…
Cancel
Save