mirror of https://github.com/SixLabors/ImageSharp
87 changed files with 3301 additions and 1330 deletions
@ -0,0 +1,101 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp |
|||
{ |
|||
/// <summary>
|
|||
/// Implements basic math operations using tolerant comparison
|
|||
/// whenever an equality check is needed.
|
|||
/// </summary>
|
|||
internal readonly struct TolerantMath |
|||
{ |
|||
private readonly double epsilon; |
|||
|
|||
private readonly double negEpsilon; |
|||
|
|||
public TolerantMath(double epsilon) |
|||
{ |
|||
DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon)); |
|||
|
|||
this.epsilon = epsilon; |
|||
this.negEpsilon = -epsilon; |
|||
} |
|||
|
|||
public static TolerantMath Default { get; } = new TolerantMath(1e-8); |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> == 0
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> > 0
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsPositive(double a) => a > this.epsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> < 0
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsNegative(double a) => a < this.negEpsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> == <paramref name="b"/>
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool AreEqual(double a, double b) => this.IsZero(a - b); |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> > <paramref name="b"/>
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsGreater(double a, double b) => a > b + this.epsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> < <paramref name="b"/>
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsLess(double a, double b) => a < b - this.epsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> >= <paramref name="b"/>
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon; |
|||
|
|||
/// <summary>
|
|||
/// <paramref name="a"/> <= <paramref name="b"/>
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public double Ceiling(double a) |
|||
{ |
|||
double rem = Math.IEEERemainder(a, 1); |
|||
if (this.IsZero(rem)) |
|||
{ |
|||
return Math.Round(a); |
|||
} |
|||
|
|||
return Math.Ceiling(a); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public double Floor(double a) |
|||
{ |
|||
double rem = Math.IEEERemainder(a, 1); |
|||
if (this.IsZero(rem)) |
|||
{ |
|||
return Math.Round(a); |
|||
} |
|||
|
|||
return Math.Floor(a); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,303 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// A helper class for constructing <see cref="Matrix3x2"/> instances for use in affine transforms.
|
|||
/// </summary>
|
|||
public class AffineTransformBuilder |
|||
{ |
|||
private readonly List<Func<Size, Matrix3x2>> matrixFactories = new List<Func<Size, Matrix3x2>>(); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a rotation matrix using the given rotation angle in degrees
|
|||
/// and the image center point as rotation center.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependRotationDegrees(float degrees) |
|||
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a rotation matrix using the given rotation angle in radians
|
|||
/// and the image center point as rotation center.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependRotationRadians(float radians) |
|||
=> this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) |
|||
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a rotation matrix using the given rotation in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 origin) |
|||
=> this.PrependMatrix(Matrix3x2.CreateRotation(radians, origin)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a rotation matrix using the given rotation angle in degrees
|
|||
/// and the image center point as rotation center.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendRotationDegrees(float degrees) |
|||
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a rotation matrix using the given rotation angle in radians
|
|||
/// and the image center point as rotation center.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendRotationRadians(float radians) |
|||
=> this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) |
|||
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); |
|||
|
|||
/// <summary>
|
|||
/// Appends a rotation matrix using the given rotation in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 origin) |
|||
=> this.AppendMatrix(Matrix3x2.CreateRotation(radians, origin)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given uniform scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The uniform scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependScale(float scale) |
|||
=> this.PrependMatrix(Matrix3x2.CreateScale(scale)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependScale(SizeF scale) |
|||
=> this.PrependScale((Vector2)scale); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependScale(Vector2 scales) |
|||
=> this.PrependMatrix(Matrix3x2.CreateScale(scales)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given uniform scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The uniform scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendScale(float scale) |
|||
=> this.AppendMatrix(Matrix3x2.CreateScale(scale)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendScale(SizeF scales) |
|||
=> this.AppendScale((Vector2)scales); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendScale(Vector2 scales) |
|||
=> this.AppendMatrix(Matrix3x2.CreateScale(scales)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered skew matrix from the give angles in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) |
|||
=> this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered skew matrix from the give angles in radians.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) |
|||
=> this.Prepend(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a skew matrix using the given angles in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) |
|||
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a skew matrix using the given angles in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) |
|||
=> this.PrependMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered skew matrix from the give angles in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) |
|||
=> this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered skew matrix from the give angles in radians.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) |
|||
=> this.Append(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a skew matrix using the given angles in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) |
|||
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); |
|||
|
|||
/// <summary>
|
|||
/// Appends a skew matrix using the given angles in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) |
|||
=> this.AppendMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependTranslation(PointF position) |
|||
=> this.PrependTranslation((Vector2)position); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependTranslation(Vector2 position) |
|||
=> this.PrependMatrix(Matrix3x2.CreateTranslation(position)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendTranslation(PointF position) |
|||
=> this.AppendTranslation((Vector2)position); |
|||
|
|||
/// <summary>
|
|||
/// Appends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendTranslation(Vector2 position) |
|||
=> this.AppendMatrix(Matrix3x2.CreateTranslation(position)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a raw matrix.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The matrix to prepend.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) => this.Prepend(_ => matrix); |
|||
|
|||
/// <summary>
|
|||
/// Appends a raw matrix.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The matrix to append.</param>
|
|||
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
|
|||
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) => this.Append(_ => matrix); |
|||
|
|||
/// <summary>
|
|||
/// Returns the combined matrix for a given source size.
|
|||
/// </summary>
|
|||
/// <param name="sourceSize">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); |
|||
|
|||
/// <summary>
|
|||
/// Returns the combined matrix for a given source rectangle.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The rectangle in the source image.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) |
|||
{ |
|||
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); |
|||
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); |
|||
|
|||
// Translate the origin matrix to cater for source rectangle offsets.
|
|||
var matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); |
|||
|
|||
Size size = sourceRectangle.Size; |
|||
|
|||
foreach (Func<Size, Matrix3x2> factory in this.matrixFactories) |
|||
{ |
|||
matrix *= factory(size); |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
|
|||
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> factory) |
|||
{ |
|||
this.matrixFactories.Insert(0, factory); |
|||
return this; |
|||
} |
|||
|
|||
private AffineTransformBuilder Append(Func<Size, Matrix3x2> factory) |
|||
{ |
|||
this.matrixFactories.Add(factory); |
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// A base class that provides methods to allow the automatic centering of affine transforms
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CenteredAffineTransformProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The transform matrix</param>
|
|||
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
|||
/// <param name="sourceSize">The source image size</param>
|
|||
protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) |
|||
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
|||
=> TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); |
|||
|
|||
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) |
|||
{ |
|||
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); |
|||
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; |
|||
} |
|||
} |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// A base class that provides methods to allow the automatic centering of non-affine transforms
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal abstract class CenteredProjectiveTransformProcessor<TPixel> : ProjectiveTransformProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CenteredProjectiveTransformProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The transform matrix</param>
|
|||
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
|||
/// <param name="sourceSize">The source image size</param>
|
|||
protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) |
|||
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
|||
{ |
|||
return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); |
|||
} |
|||
|
|||
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) |
|||
{ |
|||
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); |
|||
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; |
|||
} |
|||
} |
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// The base class for performing interpolated affine and non-affine transforms.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal abstract class InterpolatedTransformProcessorBase<TPixel> : TransformProcessorBase<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="InterpolatedTransformProcessorBase{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
|||
protected InterpolatedTransformProcessorBase(IResampler sampler) |
|||
{ |
|||
Guard.NotNull(sampler, nameof(sampler)); |
|||
this.Sampler = sampler; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the sampler to perform interpolation of the transform operation.
|
|||
/// </summary>
|
|||
public IResampler Sampler { get; } |
|||
|
|||
/// <summary>
|
|||
/// Calculated the weights for the given point.
|
|||
/// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered.
|
|||
/// Additionally the weights are normalized.
|
|||
/// </summary>
|
|||
/// <param name="min">The minimum sampling offset</param>
|
|||
/// <param name="max">The maximum sampling offset</param>
|
|||
/// <param name="sourceMin">The minimum source bounds</param>
|
|||
/// <param name="sourceMax">The maximum source bounds</param>
|
|||
/// <param name="point">The transformed point dimension</param>
|
|||
/// <param name="sampler">The sampler</param>
|
|||
/// <param name="scale">The transformed image scale relative to the source</param>
|
|||
/// <param name="weightsRef">The reference to the collection of weights</param>
|
|||
/// <param name="length">The length of the weights collection</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length) |
|||
{ |
|||
float sum = 0; |
|||
|
|||
// Downsampling weights requires more edge sampling plus normalization of the weights
|
|||
for (int x = 0, i = min; i <= max; i++, x++) |
|||
{ |
|||
int index = i; |
|||
if (index < sourceMin) |
|||
{ |
|||
index = sourceMin; |
|||
} |
|||
|
|||
if (index > sourceMax) |
|||
{ |
|||
index = sourceMax; |
|||
} |
|||
|
|||
float weight = sampler.GetValue((index - point) / scale); |
|||
sum += weight; |
|||
Unsafe.Add(ref weightsRef, x) = weight; |
|||
} |
|||
|
|||
if (sum > 0) |
|||
{ |
|||
for (int i = 0; i < length; i++) |
|||
{ |
|||
ref float wRef = ref Unsafe.Add(ref weightsRef, i); |
|||
wRef = wRef / sum; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculated the weights for the given point.
|
|||
/// </summary>
|
|||
/// <param name="sourceMin">The minimum source bounds</param>
|
|||
/// <param name="sourceMax">The maximum source bounds</param>
|
|||
/// <param name="point">The transformed point dimension</param>
|
|||
/// <param name="sampler">The sampler</param>
|
|||
/// <param name="weightsRef">The reference to the collection of weights</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef) |
|||
{ |
|||
for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) |
|||
{ |
|||
float weight = sampler.GetValue(i - point); |
|||
Unsafe.Add(ref weightsRef, x) = weight; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the sampling radius for the current sampler
|
|||
/// </summary>
|
|||
/// <param name="sourceSize">The source dimension size</param>
|
|||
/// <param name="destinationSize">The destination dimension size</param>
|
|||
/// <returns>The radius, and scaling factor</returns>
|
|||
protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) |
|||
{ |
|||
float ratio = (float)sourceSize / destinationSize; |
|||
float scale = ratio; |
|||
|
|||
if (scale < 1F) |
|||
{ |
|||
scale = 1F; |
|||
} |
|||
|
|||
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); |
|||
} |
|||
} |
|||
} |
|||
@ -1,130 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the <see cref="ResizeKernel"/> values in an optimized contigous memory region.
|
|||
/// </summary>
|
|||
internal class KernelMap : IDisposable |
|||
{ |
|||
private readonly Buffer2D<float> data; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="KernelMap"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for allocations.</param>
|
|||
/// <param name="destinationSize">The size of the destination window</param>
|
|||
/// <param name="kernelRadius">The radius of the kernel</param>
|
|||
public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) |
|||
{ |
|||
int width = (int)Math.Ceiling(kernelRadius * 2); |
|||
this.data = memoryAllocator.Allocate2D<float>(width, destinationSize, AllocationOptions.Clean); |
|||
this.Kernels = new ResizeKernel[destinationSize]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the calculated <see cref="Kernels"/> values.
|
|||
/// </summary>
|
|||
public ResizeKernel[] Kernels { get; } |
|||
|
|||
/// <summary>
|
|||
/// Disposes <see cref="KernelMap"/> instance releasing it's backing buffer.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
this.data.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes the weights to apply at each pixel when resizing.
|
|||
/// </summary>
|
|||
/// <param name="sampler">The <see cref="IResampler"/></param>
|
|||
/// <param name="destinationSize">The destination size</param>
|
|||
/// <param name="sourceSize">The source size</param>
|
|||
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
|
|||
/// <returns>The <see cref="KernelMap"/></returns>
|
|||
public static KernelMap Calculate( |
|||
IResampler sampler, |
|||
int destinationSize, |
|||
int sourceSize, |
|||
MemoryAllocator memoryAllocator) |
|||
{ |
|||
float ratio = (float)sourceSize / destinationSize; |
|||
float scale = ratio; |
|||
|
|||
if (scale < 1F) |
|||
{ |
|||
scale = 1F; |
|||
} |
|||
|
|||
float radius = MathF.Ceiling(scale * sampler.Radius); |
|||
var result = new KernelMap(memoryAllocator, destinationSize, radius); |
|||
|
|||
for (int i = 0; i < destinationSize; i++) |
|||
{ |
|||
float center = ((i + .5F) * ratio) - .5F; |
|||
|
|||
// Keep inside bounds.
|
|||
int left = (int)MathF.Ceiling(center - radius); |
|||
if (left < 0) |
|||
{ |
|||
left = 0; |
|||
} |
|||
|
|||
int right = (int)MathF.Floor(center + radius); |
|||
if (right > sourceSize - 1) |
|||
{ |
|||
right = sourceSize - 1; |
|||
} |
|||
|
|||
float sum = 0; |
|||
|
|||
ResizeKernel ws = result.CreateKernel(i, left, right); |
|||
result.Kernels[i] = ws; |
|||
|
|||
ref float weightsBaseRef = ref ws.GetStartReference(); |
|||
|
|||
for (int j = left; j <= right; j++) |
|||
{ |
|||
float weight = sampler.GetValue((j - center) / scale); |
|||
sum += weight; |
|||
|
|||
// weights[j - left] = weight:
|
|||
Unsafe.Add(ref weightsBaseRef, j - left) = weight; |
|||
} |
|||
|
|||
// Normalize, best to do it here rather than in the pixel loop later on.
|
|||
if (sum > 0) |
|||
{ |
|||
for (int w = 0; w < ws.Length; w++) |
|||
{ |
|||
// weights[w] = weights[w] / sum:
|
|||
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w); |
|||
wRef /= sum; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Slices a weights value at the given positions.
|
|||
/// </summary>
|
|||
/// <param name="destIdx">The index in destination buffer</param>
|
|||
/// <param name="leftIdx">The local left index value</param>
|
|||
/// <param name="rightIdx">The local right index value</param>
|
|||
/// <returns>The weights</returns>
|
|||
private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx) |
|||
{ |
|||
return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <content>
|
|||
/// Contains <see cref="PeriodicKernelMap"/>
|
|||
/// </content>
|
|||
internal partial class ResizeKernelMap |
|||
{ |
|||
/// <summary>
|
|||
/// Memory-optimized <see cref="ResizeKernelMap"/> where repeating rows are stored only once.
|
|||
/// </summary>
|
|||
private sealed class PeriodicKernelMap : ResizeKernelMap |
|||
{ |
|||
private readonly int period; |
|||
|
|||
private readonly int cornerInterval; |
|||
|
|||
public PeriodicKernelMap( |
|||
MemoryAllocator memoryAllocator, |
|||
IResampler sampler, |
|||
int sourceLength, |
|||
int destinationLength, |
|||
double ratio, |
|||
double scale, |
|||
int radius, |
|||
int period, |
|||
int cornerInterval) |
|||
: base( |
|||
memoryAllocator, |
|||
sampler, |
|||
sourceLength, |
|||
destinationLength, |
|||
(cornerInterval * 2) + period, |
|||
ratio, |
|||
scale, |
|||
radius) |
|||
{ |
|||
this.cornerInterval = cornerInterval; |
|||
this.period = period; |
|||
} |
|||
|
|||
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; |
|||
|
|||
protected override void Initialize() |
|||
{ |
|||
// Build top corner data + one period of the mosaic data:
|
|||
int startOfFirstRepeatedMosaic = this.cornerInterval + this.period; |
|||
|
|||
for (int i = 0; i < startOfFirstRepeatedMosaic; i++) |
|||
{ |
|||
ResizeKernel kernel = this.BuildKernel(i, i); |
|||
this.kernels[i] = kernel; |
|||
} |
|||
|
|||
// Copy the mosaics:
|
|||
int bottomStartDest = this.DestinationLength - this.cornerInterval; |
|||
for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) |
|||
{ |
|||
double center = ((i + .5) * this.ratio) - .5; |
|||
int left = (int)TolerantMath.Ceiling(center - this.radius); |
|||
ResizeKernel kernel = this.kernels[i - this.period]; |
|||
this.kernels[i] = kernel.AlterLeftValue(left); |
|||
} |
|||
|
|||
// Build bottom corner data:
|
|||
int bottomStartData = this.cornerInterval + this.period; |
|||
for (int i = 0; i < this.cornerInterval; i++) |
|||
{ |
|||
ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i); |
|||
this.kernels[bottomStartDest + i] = kernel; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,247 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Provides <see cref="ResizeKernel"/> values from an optimized,
|
|||
/// contiguous memory region.
|
|||
/// </summary>
|
|||
internal partial class ResizeKernelMap : IDisposable |
|||
{ |
|||
private static readonly TolerantMath TolerantMath = TolerantMath.Default; |
|||
|
|||
private readonly IResampler sampler; |
|||
|
|||
private readonly int sourceLength; |
|||
|
|||
private readonly double ratio; |
|||
|
|||
private readonly double scale; |
|||
|
|||
private readonly int radius; |
|||
|
|||
private readonly MemoryHandle pinHandle; |
|||
|
|||
private readonly Buffer2D<float> data; |
|||
|
|||
private readonly ResizeKernel[] kernels; |
|||
|
|||
// To avoid both GC allocations, and MemoryAllocator ceremony:
|
|||
private readonly double[] tempValues; |
|||
|
|||
private ResizeKernelMap( |
|||
MemoryAllocator memoryAllocator, |
|||
IResampler sampler, |
|||
int sourceLength, |
|||
int destinationLength, |
|||
int bufferHeight, |
|||
double ratio, |
|||
double scale, |
|||
int radius) |
|||
{ |
|||
this.sampler = sampler; |
|||
this.ratio = ratio; |
|||
this.scale = scale; |
|||
this.radius = radius; |
|||
this.sourceLength = sourceLength; |
|||
this.DestinationLength = destinationLength; |
|||
int maxWidth = (radius * 2) + 1; |
|||
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean); |
|||
this.pinHandle = this.data.Memory.Pin(); |
|||
this.kernels = new ResizeKernel[destinationLength]; |
|||
this.tempValues = new double[maxWidth]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length of the destination row/column
|
|||
/// </summary>
|
|||
public int DestinationLength { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a string of information to help debugging
|
|||
/// </summary>
|
|||
internal virtual string Info => |
|||
$"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}"; |
|||
|
|||
/// <summary>
|
|||
/// Disposes <see cref="ResizeKernelMap"/> instance releasing it's backing buffer.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
this.pinHandle.Dispose(); |
|||
this.data.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a <see cref="ResizeKernel"/> for an index value between 0 and DestinationSize - 1.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx]; |
|||
|
|||
/// <summary>
|
|||
/// Computes the weights to apply at each pixel when resizing.
|
|||
/// </summary>
|
|||
/// <param name="sampler">The <see cref="IResampler"/></param>
|
|||
/// <param name="destinationSize">The destination size</param>
|
|||
/// <param name="sourceSize">The source size</param>
|
|||
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
|
|||
/// <returns>The <see cref="ResizeKernelMap"/></returns>
|
|||
public static ResizeKernelMap Calculate( |
|||
IResampler sampler, |
|||
int destinationSize, |
|||
int sourceSize, |
|||
MemoryAllocator memoryAllocator) |
|||
{ |
|||
double ratio = (double)sourceSize / destinationSize; |
|||
double scale = ratio; |
|||
|
|||
if (scale < 1) |
|||
{ |
|||
scale = 1; |
|||
} |
|||
|
|||
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); |
|||
|
|||
// 'ratio' is a rational number.
|
|||
// Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
|
|||
// This value is determining the length of the periods in repeating kernel map rows.
|
|||
int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; |
|||
|
|||
// the center position at i == 0:
|
|||
double center0 = (ratio - 1) * 0.5; |
|||
double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; |
|||
|
|||
// The number of rows building a "stairway" at the top and the bottom of the kernel map
|
|||
// corresponding to the corners of the image.
|
|||
// If we do not normalize the kernel values, these rows also fit the periodic logic,
|
|||
// however, it's just simpler to calculate them separately.
|
|||
int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); |
|||
|
|||
// If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1
|
|||
// instead of Ceiling:
|
|||
if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) |
|||
{ |
|||
cornerInterval++; |
|||
} |
|||
|
|||
// If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization.
|
|||
// If we don't have at least 2 periods, we go with the basic implementation:
|
|||
bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; |
|||
|
|||
ResizeKernelMap result = hasAtLeast2Periods |
|||
? new PeriodicKernelMap( |
|||
memoryAllocator, |
|||
sampler, |
|||
sourceSize, |
|||
destinationSize, |
|||
ratio, |
|||
scale, |
|||
radius, |
|||
period, |
|||
cornerInterval) |
|||
: new ResizeKernelMap( |
|||
memoryAllocator, |
|||
sampler, |
|||
sourceSize, |
|||
destinationSize, |
|||
destinationSize, |
|||
ratio, |
|||
scale, |
|||
radius); |
|||
|
|||
result.Initialize(); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
protected virtual void Initialize() |
|||
{ |
|||
for (int i = 0; i < this.DestinationLength; i++) |
|||
{ |
|||
ResizeKernel kernel = this.BuildKernel(i, i); |
|||
this.kernels[i] = kernel; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Builds a <see cref="ResizeKernel"/> for the row <paramref name="destRowIndex"/> (in <see cref="kernels"/>)
|
|||
/// referencing the data at row <paramref name="dataRowIndex"/> within <see cref="data"/>,
|
|||
/// so the data reusable by other data rows.
|
|||
/// </summary>
|
|||
private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex) |
|||
{ |
|||
double center = ((destRowIndex + .5) * this.ratio) - .5; |
|||
|
|||
// Keep inside bounds.
|
|||
int left = (int)TolerantMath.Ceiling(center - this.radius); |
|||
if (left < 0) |
|||
{ |
|||
left = 0; |
|||
} |
|||
|
|||
int right = (int)TolerantMath.Floor(center + this.radius); |
|||
if (right > this.sourceLength - 1) |
|||
{ |
|||
right = this.sourceLength - 1; |
|||
} |
|||
|
|||
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); |
|||
|
|||
Span<double> kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length); |
|||
double sum = 0; |
|||
|
|||
for (int j = left; j <= right; j++) |
|||
{ |
|||
double value = this.sampler.GetValue((float)((j - center) / this.scale)); |
|||
sum += value; |
|||
|
|||
kernelValues[j - left] = value; |
|||
} |
|||
|
|||
// Normalize, best to do it here rather than in the pixel loop later on.
|
|||
if (sum > 0) |
|||
{ |
|||
for (int j = 0; j < kernel.Length; j++) |
|||
{ |
|||
// weights[w] = weights[w] / sum:
|
|||
ref double kRef = ref kernelValues[j]; |
|||
kRef /= sum; |
|||
} |
|||
} |
|||
|
|||
kernel.Fill(kernelValues); |
|||
|
|||
return kernel; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a <see cref="ResizeKernel"/> referencing values of <see cref="data"/>
|
|||
/// at row <paramref name="dataRowIndex"/>.
|
|||
/// </summary>
|
|||
private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right) |
|||
{ |
|||
int length = right - left + 1; |
|||
|
|||
if (length > this.data.Width) |
|||
{ |
|||
throw new InvalidOperationException( |
|||
$"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width"); |
|||
} |
|||
|
|||
Span<float> rowSpan = this.data.GetRowSpan(dataRowIndex); |
|||
|
|||
ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); |
|||
float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); |
|||
return new ResizeKernel(left, rowPtr, length); |
|||
} |
|||
} |
|||
} |
|||
@ -1,158 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.MetaData.Profiles.Exif; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains helper methods for working with affine and non-affine transforms
|
|||
/// </summary>
|
|||
internal static class TransformHelpers |
|||
{ |
|||
/// <summary>
|
|||
/// Updates the dimensional metadata of a transformed image
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="image">The image to update</param>
|
|||
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
ExifProfile profile = image.MetaData.ExifProfile; |
|||
if (profile is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Removing the previously stored value allows us to set a value with our own data tag if required.
|
|||
if (profile.GetValue(ExifTag.PixelXDimension) != null) |
|||
{ |
|||
profile.RemoveValue(ExifTag.PixelXDimension); |
|||
|
|||
if (image.Width <= ushort.MaxValue) |
|||
{ |
|||
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width); |
|||
} |
|||
else |
|||
{ |
|||
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width); |
|||
} |
|||
} |
|||
|
|||
if (profile.GetValue(ExifTag.PixelYDimension) != null) |
|||
{ |
|||
profile.RemoveValue(ExifTag.PixelYDimension); |
|||
|
|||
if (image.Height <= ushort.MaxValue) |
|||
{ |
|||
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); |
|||
} |
|||
else |
|||
{ |
|||
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the centered transform matrix based upon the source and destination rectangles
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="destinationRectangle">The destination image bounds.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/></returns>
|
|||
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) |
|||
{ |
|||
// We invert the matrix to handle the transformation from screen to world space.
|
|||
// This ensures scaling matrices are correct.
|
|||
Matrix3x2.Invert(matrix, out Matrix3x2 inverted); |
|||
|
|||
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); |
|||
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); |
|||
|
|||
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); |
|||
|
|||
// Translate back to world to pass to the Transform method.
|
|||
return centered; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the centered transform matrix based upon the source and destination rectangles
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="destinationRectangle">The destination image bounds.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/></returns>
|
|||
public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix) |
|||
{ |
|||
// We invert the matrix to handle the transformation from screen to world space.
|
|||
// This ensures scaling matrices are correct.
|
|||
Matrix4x4.Invert(matrix, out Matrix4x4 inverted); |
|||
|
|||
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); |
|||
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0); |
|||
|
|||
Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered); |
|||
|
|||
// Translate back to world to pass to the Transform method.
|
|||
return centered; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) |
|||
{ |
|||
// Calculate the position of the four corners in world space by applying
|
|||
// The world matrix to the four corners in object space (0, 0, width, height)
|
|||
var tl = Vector2.Transform(Vector2.Zero, matrix); |
|||
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); |
|||
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); |
|||
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); |
|||
|
|||
return GetBoundingRectangle(tl, tr, bl, br); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix4x4 matrix) |
|||
{ |
|||
// Calculate the position of the four corners in world space by applying
|
|||
// The world matrix to the four corners in object space (0, 0, width, height)
|
|||
var tl = Vector2.Transform(Vector2.Zero, matrix); |
|||
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); |
|||
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); |
|||
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); |
|||
|
|||
return GetBoundingRectangle(tl, tr, bl, br); |
|||
} |
|||
|
|||
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) |
|||
{ |
|||
// Find the minimum and maximum "corners" based on the given vectors
|
|||
float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); |
|||
float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); |
|||
float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); |
|||
float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); |
|||
float sizeX = maxX - minX + .5F; |
|||
float sizeY = maxY - minY + .5F; |
|||
|
|||
return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains the methods required to calculate transform kernel convolution.
|
|||
/// </summary>
|
|||
internal class TransformKernelMap : IDisposable |
|||
{ |
|||
private readonly Buffer2D<float> yBuffer; |
|||
private readonly Buffer2D<float> xBuffer; |
|||
private readonly Vector2 extents; |
|||
private Vector4 maxSourceExtents; |
|||
private readonly IResampler sampler; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TransformKernelMap"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="source">The source size.</param>
|
|||
/// <param name="destination">The destination size.</param>
|
|||
/// <param name="sampler">The sampler.</param>
|
|||
public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) |
|||
{ |
|||
this.sampler = sampler; |
|||
float yRadius = this.GetSamplingRadius(source.Height, destination.Height); |
|||
float xRadius = this.GetSamplingRadius(source.Width, destination.Width); |
|||
|
|||
this.extents = new Vector2(xRadius, yRadius); |
|||
int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); |
|||
int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); |
|||
|
|||
// We use 2D buffers so that we can access the weight spans per row in parallel.
|
|||
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height); |
|||
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height); |
|||
|
|||
int maxX = source.Width - 1; |
|||
int maxY = source.Height - 1; |
|||
this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the first item of the y window.
|
|||
/// </summary>
|
|||
/// <returns>The reference to the first item of the window.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ref float GetYStartReference(int y) |
|||
=> ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y)); |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the first item of the x window.
|
|||
/// </summary>
|
|||
/// <returns>The reference to the first item of the window.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ref float GetXStartReference(int y) |
|||
=> ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y)); |
|||
|
|||
public void Convolve<TPixel>( |
|||
Vector2 transformedPoint, |
|||
int column, |
|||
ref float ySpanRef, |
|||
ref float xSpanRef, |
|||
Buffer2D<TPixel> sourcePixels, |
|||
Span<Vector4> targetRow) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// Clamp sampling pixel radial extents to the source image edges
|
|||
Vector2 minXY = transformedPoint - this.extents; |
|||
Vector2 maxXY = transformedPoint + this.extents; |
|||
|
|||
// left, top, right, bottom
|
|||
var extents = new Vector4( |
|||
MathF.Ceiling(minXY.X - .5F), |
|||
MathF.Ceiling(minXY.Y - .5F), |
|||
MathF.Floor(maxXY.X + .5F), |
|||
MathF.Floor(maxXY.Y + .5F)); |
|||
|
|||
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); |
|||
|
|||
int left = (int)extents.X; |
|||
int top = (int)extents.Y; |
|||
int right = (int)extents.Z; |
|||
int bottom = (int)extents.W; |
|||
|
|||
if (left == right || top == bottom) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef); |
|||
this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef); |
|||
|
|||
Vector4 sum = Vector4.Zero; |
|||
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++) |
|||
{ |
|||
float yWeight = Unsafe.Add(ref ySpanRef, kernelY); |
|||
|
|||
for (int kernelX = 0, x = left; x <= right; x++, kernelX++) |
|||
{ |
|||
float xWeight = Unsafe.Add(ref xSpanRef, kernelX); |
|||
|
|||
// Values are first premultiplied to prevent darkening of edge pixels.
|
|||
var current = sourcePixels[x, y].ToVector4(); |
|||
Vector4Utils.Premultiply(ref current); |
|||
sum += current * xWeight * yWeight; |
|||
} |
|||
} |
|||
|
|||
// Reverse the premultiplication
|
|||
Vector4Utils.UnPremultiply(ref sum); |
|||
targetRow[column] = sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculated the normalized weights for the given point.
|
|||
/// </summary>
|
|||
/// <param name="min">The minimum sampling offset</param>
|
|||
/// <param name="max">The maximum sampling offset</param>
|
|||
/// <param name="point">The transformed point dimension</param>
|
|||
/// <param name="weightsRef">The reference to the collection of weights</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void CalculateWeights(int min, int max, float point, ref float weightsRef) |
|||
{ |
|||
float sum = 0; |
|||
for (int x = 0, i = min; i <= max; i++, x++) |
|||
{ |
|||
float weight = this.sampler.GetValue(i - point); |
|||
sum += weight; |
|||
Unsafe.Add(ref weightsRef, x) = weight; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private float GetSamplingRadius(int sourceSize, int destinationSize) |
|||
{ |
|||
float scale = (float)sourceSize / destinationSize; |
|||
|
|||
if (scale < 1F) |
|||
{ |
|||
scale = 1F; |
|||
} |
|||
|
|||
return MathF.Ceiling(scale * this.sampler.Radius); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.yBuffer?.Dispose(); |
|||
this.xBuffer?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.MetaData.Profiles.Exif; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains helper methods for working with transforms.
|
|||
/// </summary>
|
|||
internal static class TransformProcessorHelpers |
|||
{ |
|||
/// <summary>
|
|||
/// Updates the dimensional metadata of a transformed image
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="image">The image to update</param>
|
|||
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
ExifProfile profile = image.MetaData.ExifProfile; |
|||
if (profile is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Removing the previously stored value allows us to set a value with our own data tag if required.
|
|||
if (profile.GetValue(ExifTag.PixelXDimension) != null) |
|||
{ |
|||
profile.RemoveValue(ExifTag.PixelXDimension); |
|||
|
|||
if (image.Width <= ushort.MaxValue) |
|||
{ |
|||
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width); |
|||
} |
|||
else |
|||
{ |
|||
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width); |
|||
} |
|||
} |
|||
|
|||
if (profile.GetValue(ExifTag.PixelYDimension) != null) |
|||
{ |
|||
profile.RemoveValue(ExifTag.PixelYDimension); |
|||
|
|||
if (image.Height <= ushort.MaxValue) |
|||
{ |
|||
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); |
|||
} |
|||
else |
|||
{ |
|||
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,332 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains utility methods for working with transforms.
|
|||
/// </summary>
|
|||
internal static class TransformUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a centered rotation matrix using the given rotation in degrees and the source size.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <param name="size">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size) |
|||
=> CreateCenteredTransformMatrix( |
|||
new Rectangle(Point.Empty, size), |
|||
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); |
|||
|
|||
/// <summary>
|
|||
/// Creates a centered rotation matrix using the given rotation in radians and the source size.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <param name="size">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size) |
|||
=> CreateCenteredTransformMatrix( |
|||
new Rectangle(Point.Empty, size), |
|||
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); |
|||
|
|||
/// <summary>
|
|||
/// Creates a centered skew matrix from the give angles in degrees and the source size.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <param name="size">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size) |
|||
=> CreateCenteredTransformMatrix( |
|||
new Rectangle(Point.Empty, size), |
|||
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); |
|||
|
|||
/// <summary>
|
|||
/// Creates a centered skew matrix from the give angles in radians and the source size.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <param name="size">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public static Matrix3x2 CreateSkewMatrixRadians(float radiansX, float radiansY, Size size) |
|||
=> CreateCenteredTransformMatrix( |
|||
new Rectangle(Point.Empty, size), |
|||
Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the centered transform matrix based upon the source and destination rectangles.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/></returns>
|
|||
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) |
|||
{ |
|||
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); |
|||
|
|||
// We invert the matrix to handle the transformation from screen to world space.
|
|||
// This ensures scaling matrices are correct.
|
|||
Matrix3x2.Invert(matrix, out Matrix3x2 inverted); |
|||
|
|||
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); |
|||
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); |
|||
|
|||
// Translate back to world space.
|
|||
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); |
|||
|
|||
return centered; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a matrix that performs a tapering projective transform.
|
|||
/// <see href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine"/>
|
|||
/// </summary>
|
|||
/// <param name="size">The rectangular size of the image being transformed.</param>
|
|||
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
|
|||
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
|
|||
/// <param name="fraction">The amount to taper.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/></returns>
|
|||
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction) |
|||
{ |
|||
Matrix4x4 matrix = Matrix4x4.Identity; |
|||
|
|||
switch (side) |
|||
{ |
|||
case TaperSide.Left: |
|||
matrix.M11 = fraction; |
|||
matrix.M22 = fraction; |
|||
matrix.M13 = (fraction - 1) / size.Width; |
|||
|
|||
switch (corner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - fraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = size.Height * .5F * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - fraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Top: |
|||
matrix.M11 = fraction; |
|||
matrix.M22 = fraction; |
|||
matrix.M23 = (fraction - 1) / size.Height; |
|||
|
|||
switch (corner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - fraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = size.Width * .5F * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - fraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Right: |
|||
matrix.M11 = 1 / fraction; |
|||
matrix.M13 = (1 - fraction) / (size.Width * fraction); |
|||
|
|||
switch (corner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = size.Height * .5F * matrix.M13; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Bottom: |
|||
matrix.M22 = 1 / fraction; |
|||
matrix.M23 = (1 - fraction) / (size.Height * fraction); |
|||
|
|||
switch (corner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = size.Width * .5F * matrix.M23; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) |
|||
{ |
|||
Rectangle transformed = GetTransformedRectangle(rectangle, matrix); |
|||
return new Rectangle(0, 0, transformed.Width, transformed.Height); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rectangle relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) |
|||
{ |
|||
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) |
|||
{ |
|||
return rectangle; |
|||
} |
|||
|
|||
var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); |
|||
var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); |
|||
var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); |
|||
var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); |
|||
|
|||
return GetBoundingRectangle(tl, tr, bl, br); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the size relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="size">The source size.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Size"/>.
|
|||
/// </returns>
|
|||
public static Size GetTransformedSize(Size size, Matrix3x2 matrix) |
|||
{ |
|||
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); |
|||
|
|||
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) |
|||
{ |
|||
return size; |
|||
} |
|||
|
|||
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); |
|||
|
|||
return ConstrainSize(rectangle); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rectangle relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) |
|||
{ |
|||
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) |
|||
{ |
|||
return rectangle; |
|||
} |
|||
|
|||
Vector2 GetVector(float x, float y) |
|||
{ |
|||
const float Epsilon = 0.0000001F; |
|||
var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); |
|||
return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); |
|||
} |
|||
|
|||
Vector2 tl = GetVector(rectangle.Left, rectangle.Top); |
|||
Vector2 tr = GetVector(rectangle.Right, rectangle.Top); |
|||
Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom); |
|||
Vector2 br = GetVector(rectangle.Right, rectangle.Bottom); |
|||
|
|||
return GetBoundingRectangle(tl, tr, bl, br); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the size relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="size">The source size.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Size"/>.
|
|||
/// </returns>
|
|||
public static Size GetTransformedSize(Size size, Matrix4x4 matrix) |
|||
{ |
|||
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); |
|||
|
|||
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) |
|||
{ |
|||
return size; |
|||
} |
|||
|
|||
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); |
|||
|
|||
return ConstrainSize(rectangle); |
|||
} |
|||
|
|||
private static Size ConstrainSize(Rectangle rectangle) |
|||
{ |
|||
// We want to resize the canvas here taking into account any translations.
|
|||
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); |
|||
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); |
|||
|
|||
// If location in either direction is translated to a negative value equal to or exceeding the
|
|||
// dimensions in eith direction we need to reassign the dimension.
|
|||
if (height <= 0) |
|||
{ |
|||
height = rectangle.Height; |
|||
} |
|||
|
|||
if (width <= 0) |
|||
{ |
|||
width = rectangle.Width; |
|||
} |
|||
|
|||
return new Size(width, height); |
|||
} |
|||
|
|||
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) |
|||
{ |
|||
// Find the minimum and maximum "corners" based on the given vectors
|
|||
float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); |
|||
float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); |
|||
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); |
|||
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); |
|||
|
|||
return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,319 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// A helper class for constructing <see cref="Matrix4x4"/> instances for use in projective transforms.
|
|||
/// </summary>
|
|||
public class ProjectiveTransformBuilder |
|||
{ |
|||
private readonly List<Func<Size, Matrix4x4>> matrixFactories = new List<Func<Size, Matrix4x4>>(); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a matrix that performs a tapering projective transform.
|
|||
/// </summary>
|
|||
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
|
|||
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
|
|||
/// <param name="fraction">The amount to taper.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) |
|||
=> this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a matrix that performs a tapering projective transform.
|
|||
/// </summary>
|
|||
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
|
|||
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
|
|||
/// <param name="fraction">The amount to taper.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) |
|||
=> this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered rotation matrix using the given rotation in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) |
|||
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered rotation matrix using the given rotation in radians.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependRotationRadians(float radians) |
|||
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) |
|||
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered rotation matrix using the given rotation in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin) |
|||
=> this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered rotation matrix using the given rotation in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) |
|||
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered rotation matrix using the given rotation in radians.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendRotationRadians(float radians) |
|||
=> this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) |
|||
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered rotation matrix using the given rotation in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radians">The amount of rotation, in radians.</param>
|
|||
/// <param name="origin">The rotation origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 origin) |
|||
=> this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given uniform scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The uniform scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependScale(float scale) |
|||
=> this.PrependMatrix(Matrix4x4.CreateScale(scale)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependScale(SizeF scale) |
|||
=> this.PrependScale((Vector2)scale); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependScale(Vector2 scales) |
|||
=> this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given uniform scale.
|
|||
/// </summary>
|
|||
/// <param name="scale">The uniform scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendScale(float scale) |
|||
=> this.AppendMatrix(Matrix4x4.CreateScale(scale)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendScale(SizeF scales) |
|||
=> this.AppendScale((Vector2)scales); |
|||
|
|||
/// <summary>
|
|||
/// Appends a scale matrix from the given vector scale.
|
|||
/// </summary>
|
|||
/// <param name="scales">The horizontal and vertical scale.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendScale(Vector2 scales) |
|||
=> this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered skew matrix from the give angles in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) |
|||
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a centered skew matrix from the give angles in radians.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) |
|||
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a skew matrix using the given angles in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) |
|||
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a skew matrix using the given angles in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) |
|||
=> this.PrependMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered skew matrix from the give angles in degrees.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) |
|||
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); |
|||
|
|||
/// <summary>
|
|||
/// Appends a centered skew matrix from the give angles in radians.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) |
|||
=> this.Append(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a skew matrix using the given angles in degrees at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="degreesX">The X angle, in degrees.</param>
|
|||
/// <param name="degreesY">The Y angle, in degrees.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) |
|||
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); |
|||
|
|||
/// <summary>
|
|||
/// Appends a skew matrix using the given angles in radians at the given origin.
|
|||
/// </summary>
|
|||
/// <param name="radiansX">The X angle, in radians.</param>
|
|||
/// <param name="radiansY">The Y angle, in radians.</param>
|
|||
/// <param name="origin">The skew origin point.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) |
|||
=> this.AppendMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependTranslation(PointF position) |
|||
=> this.PrependTranslation((Vector2)position); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependTranslation(Vector2 position) |
|||
=> this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); |
|||
|
|||
/// <summary>
|
|||
/// Appends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendTranslation(PointF position) |
|||
=> this.AppendTranslation((Vector2)position); |
|||
|
|||
/// <summary>
|
|||
/// Appends a translation matrix from the given vector.
|
|||
/// </summary>
|
|||
/// <param name="position">The translation position.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendTranslation(Vector2 position) |
|||
=> this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); |
|||
|
|||
/// <summary>
|
|||
/// Prepends a raw matrix.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The matrix to prepend.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) => this.Prepend(_ => matrix); |
|||
|
|||
/// <summary>
|
|||
/// Appends a raw matrix.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The matrix to append.</param>
|
|||
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
|
|||
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) => this.Append(_ => matrix); |
|||
|
|||
/// <summary>
|
|||
/// Returns the combined matrix for a given source size.
|
|||
/// </summary>
|
|||
/// <param name="sourceSize">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/>.</returns>
|
|||
public Matrix4x4 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); |
|||
|
|||
/// <summary>
|
|||
/// Returns the combined matrix for a given source rectangle.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The rectangle in the source image.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/>.</returns>
|
|||
public Matrix4x4 BuildMatrix(Rectangle sourceRectangle) |
|||
{ |
|||
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); |
|||
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); |
|||
|
|||
// Translate the origin matrix to cater for source rectangle offsets.
|
|||
var matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); |
|||
|
|||
Size size = sourceRectangle.Size; |
|||
|
|||
foreach (Func<Size, Matrix4x4> factory in this.matrixFactories) |
|||
{ |
|||
matrix *= factory(size); |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
|
|||
private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> factory) |
|||
{ |
|||
this.matrixFactories.Insert(0, factory); |
|||
return this; |
|||
} |
|||
|
|||
private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> factory) |
|||
{ |
|||
this.matrixFactories.Add(factory); |
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -1,166 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Enumerates the various options which determine which side to taper
|
|||
/// </summary>
|
|||
public enum TaperSide |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left side
|
|||
/// </summary>
|
|||
Left, |
|||
|
|||
/// <summary>
|
|||
/// Taper the top side
|
|||
/// </summary>
|
|||
Top, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right side
|
|||
/// </summary>
|
|||
Right, |
|||
|
|||
/// <summary>
|
|||
/// Taper the bottom side
|
|||
/// </summary>
|
|||
Bottom |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Enumerates the various options which determine how to taper corners
|
|||
/// </summary>
|
|||
public enum TaperCorner |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left or top corner
|
|||
/// </summary>
|
|||
LeftOrTop, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right or bottom corner
|
|||
/// </summary>
|
|||
RightOrBottom, |
|||
|
|||
/// <summary>
|
|||
/// Taper the both sets of corners
|
|||
/// </summary>
|
|||
Both |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides helper methods for working with generalized projective transforms.
|
|||
/// </summary>
|
|||
public static class ProjectiveTransformHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a matrix that performs a tapering projective transform.
|
|||
/// <see href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine"/>
|
|||
/// </summary>
|
|||
/// <param name="size">The rectangular size of the image being transformed.</param>
|
|||
/// <param name="taperSide">An enumeration that indicates the side of the rectangle that tapers.</param>
|
|||
/// <param name="taperCorner">An enumeration that indicates on which corners to taper the rectangle.</param>
|
|||
/// <param name="taperFraction">The amount to taper.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/></returns>
|
|||
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction) |
|||
{ |
|||
Matrix4x4 matrix = Matrix4x4.Identity; |
|||
|
|||
switch (taperSide) |
|||
{ |
|||
case TaperSide.Left: |
|||
matrix.M11 = taperFraction; |
|||
matrix.M22 = taperFraction; |
|||
matrix.M13 = (taperFraction - 1) / size.Width; |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - taperFraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = (size.Height * 0.5f) * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - taperFraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Top: |
|||
matrix.M11 = taperFraction; |
|||
matrix.M22 = taperFraction; |
|||
matrix.M23 = (taperFraction - 1) / size.Height; |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - taperFraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = (size.Width * 0.5f) * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - taperFraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Right: |
|||
matrix.M11 = 1 / taperFraction; |
|||
matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction); |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = (size.Height * 0.5f) * matrix.M13; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Bottom: |
|||
matrix.M22 = 1 / taperFraction; |
|||
matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction); |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = (size.Width * 0.5f) * matrix.M23; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Enumerates the various options which determine how to taper corners
|
|||
/// </summary>
|
|||
public enum TaperCorner |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left or top corner
|
|||
/// </summary>
|
|||
LeftOrTop, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right or bottom corner
|
|||
/// </summary>
|
|||
RightOrBottom, |
|||
|
|||
/// <summary>
|
|||
/// Taper the both sets of corners
|
|||
/// </summary>
|
|||
Both |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Enumerates the various options which determine which side to taper
|
|||
/// </summary>
|
|||
public enum TaperSide |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left side
|
|||
/// </summary>
|
|||
Left, |
|||
|
|||
/// <summary>
|
|||
/// Taper the top side
|
|||
/// </summary>
|
|||
Top, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right side
|
|||
/// </summary>
|
|||
Right, |
|||
|
|||
/// <summary>
|
|||
/// Taper the bottom side
|
|||
/// </summary>
|
|||
Bottom |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Samplers |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class Rotate |
|||
{ |
|||
[Benchmark] |
|||
public Size DoRotate() |
|||
{ |
|||
using (var image = new Image<Rgba32>(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) |
|||
{ |
|||
image.Mutate(x => x.Rotate(37.5F)); |
|||
|
|||
return image.Size(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Nov 7 2018
|
|||
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
|
|||
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
|
|||
//.NET Core SDK = 2.1.403
|
|||
|
|||
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
|
|||
// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
|
|||
//LaunchCount=1 TargetCount=3 WarmupCount=3
|
|||
|
|||
// #### BEFORE ####:
|
|||
// Method | Runtime | Mean | Error | StdDev | Allocated |
|
|||
//--------- |-------- |---------:|----------:|----------:|----------:|
|
|||
// DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB |
|
|||
// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB |
|
|||
|
|||
// #### AFTER ####:
|
|||
//Method | Runtime | Mean | Error | StdDev | Allocated |
|
|||
//--------- |-------- |---------:|---------:|---------:|----------:|
|
|||
// DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB |
|
|||
// DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB |
|
|||
@ -0,0 +1,45 @@ |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Samplers |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class Skew |
|||
{ |
|||
[Benchmark] |
|||
public Size DoSkew() |
|||
{ |
|||
using (var image = new Image<Rgba32>(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) |
|||
{ |
|||
image.Mutate(x => x.Skew(20, 10)); |
|||
|
|||
return image.Size(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Nov 7 2018
|
|||
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
|
|||
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
|
|||
//.NET Core SDK = 2.1.403
|
|||
|
|||
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
|
|||
// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
|
|||
//LaunchCount=1 TargetCount=3 WarmupCount=3
|
|||
|
|||
// #### BEFORE ####:
|
|||
//Method | Runtime | Mean | Error | StdDev | Allocated |
|
|||
//------- |-------- |---------:|---------:|----------:|----------:|
|
|||
// DoSkew | Clr | 78.14 ms | 8.383 ms | 0.4736 ms | 6 KB |
|
|||
// DoSkew | Core | 44.22 ms | 4.109 ms | 0.2322 ms | 4.28 KB |
|
|||
|
|||
// #### AFTER ####:
|
|||
//Method | Runtime | Mean | Error | StdDev | Allocated |
|
|||
//------- |-------- |---------:|----------:|----------:|----------:|
|
|||
// DoSkew | Clr | 71.63 ms | 25.589 ms | 1.4458 ms | 6 KB |
|
|||
// DoSkew | Core | 38.99 ms | 8.640 ms | 0.4882 ms | 4.36 KB |
|
|||
@ -0,0 +1,168 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using Xunit; |
|||
// ReSharper disable InconsistentNaming
|
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Helpers |
|||
{ |
|||
public class TolerantMathTests |
|||
{ |
|||
private readonly TolerantMath tolerantMath = new TolerantMath(0.1); |
|||
|
|||
[Theory] |
|||
[InlineData(0)] |
|||
[InlineData(0.01)] |
|||
[InlineData(-0.05)] |
|||
public void IsZero_WhenTrue(double a) |
|||
{ |
|||
Assert.True(this.tolerantMath.IsZero(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0.11)] |
|||
[InlineData(-0.101)] |
|||
[InlineData(42)] |
|||
public void IsZero_WhenFalse(double a) |
|||
{ |
|||
Assert.False(this.tolerantMath.IsZero(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0.11)] |
|||
[InlineData(100)] |
|||
public void IsPositive_WhenTrue(double a) |
|||
{ |
|||
Assert.True(this.tolerantMath.IsPositive(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0.09)] |
|||
[InlineData(-0.1)] |
|||
[InlineData(-1000)] |
|||
public void IsPositive_WhenFalse(double a) |
|||
{ |
|||
Assert.False(this.tolerantMath.IsPositive(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(-0.11)] |
|||
[InlineData(-100)] |
|||
public void IsNegative_WhenTrue(double a) |
|||
{ |
|||
Assert.True(this.tolerantMath.IsNegative(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(-0.09)] |
|||
[InlineData(0.1)] |
|||
[InlineData(1000)] |
|||
public void IsNegative_WhenFalse(double a) |
|||
{ |
|||
Assert.False(this.tolerantMath.IsNegative(a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(4.2, 4.2)] |
|||
[InlineData(4.2, 4.25)] |
|||
[InlineData(-Math.PI, -Math.PI + 0.05)] |
|||
[InlineData(999999.2, 999999.25)] |
|||
public void AreEqual_WhenTrue(double a, double b) |
|||
{ |
|||
Assert.True(this.tolerantMath.AreEqual(a, b)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1, 2)] |
|||
[InlineData(-1000000, -1000000.2)] |
|||
public void AreEqual_WhenFalse(double a, double b) |
|||
{ |
|||
Assert.False(this.tolerantMath.AreEqual(a, b)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(2, 1.8)] |
|||
[InlineData(-20, -20.2)] |
|||
[InlineData(0.1, -0.1)] |
|||
[InlineData(100, 10)] |
|||
public void IsGreater_IsLess_WhenTrue(double a, double b) |
|||
{ |
|||
Assert.True(this.tolerantMath.IsGreater(a, b)); |
|||
Assert.True(this.tolerantMath.IsLess(b, a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(2, 1.95)] |
|||
[InlineData(-20, -20.02)] |
|||
[InlineData(0.01, -0.01)] |
|||
[InlineData(999999, 999999.09)] |
|||
public void IsGreater_IsLess_WhenFalse(double a, double b) |
|||
{ |
|||
Assert.False(this.tolerantMath.IsGreater(a, b)); |
|||
Assert.False(this.tolerantMath.IsLess(b, a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(3, 2)] |
|||
[InlineData(3, 2.99)] |
|||
[InlineData(2.99, 3)] |
|||
[InlineData(-5, -6)] |
|||
[InlineData(-5, -5.05)] |
|||
[InlineData(-5.05, -5)] |
|||
public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b) |
|||
{ |
|||
Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b)); |
|||
Assert.True(this.tolerantMath.IsLessOrEqual(b, a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(2, 3)] |
|||
[InlineData(2.89, 3)] |
|||
[InlineData(-3, -2.89)] |
|||
public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b) |
|||
{ |
|||
Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b)); |
|||
Assert.False(this.tolerantMath.IsLessOrEqual(b, a)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(3.5, 4.0)] |
|||
[InlineData(3.89, 4.0)] |
|||
[InlineData(4.09, 4.0)] |
|||
[InlineData(4.11, 5.0)] |
|||
[InlineData(0.11, 1)] |
|||
[InlineData(0.05, 0)] |
|||
[InlineData(-0.5, 0)] |
|||
[InlineData(-0.95, -1)] |
|||
[InlineData(-1.05, -1)] |
|||
[InlineData(-1.5, -1)] |
|||
public void Ceiling(double value, double expected) |
|||
{ |
|||
double actual = this.tolerantMath.Ceiling(value); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1, 1)] |
|||
[InlineData(0.99, 1)] |
|||
[InlineData(0.5, 0)] |
|||
[InlineData(0.01, 0)] |
|||
[InlineData(-0.09, 0)] |
|||
[InlineData(-0.11, -1)] |
|||
[InlineData(-100.11, -101)] |
|||
[InlineData(-100.09, -100)] |
|||
public void Floor(double value, double expected) |
|||
{ |
|||
double plz1 = Math.IEEERemainder(1.1, 1); |
|||
double plz2 = Math.IEEERemainder(0.9, 1); |
|||
|
|||
double plz3 = Math.IEEERemainder(-1.1, 1); |
|||
double plz4 = Math.IEEERemainder(-0.9, 1); |
|||
|
|||
double actual = this.tolerantMath.Floor(value); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
} |
|||
} |
|||
@ -1,61 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
using SixLabors.Primitives; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
|||
{ |
|||
public class KernelMapTests |
|||
{ |
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
public KernelMapTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
[Theory(Skip = "TODO: Add asserionts")] |
|||
[InlineData(500, 200, nameof(KnownResamplers.Bicubic))] |
|||
[InlineData(50, 40, nameof(KnownResamplers.Bicubic))] |
|||
[InlineData(40, 30, nameof(KnownResamplers.Bicubic))] |
|||
[InlineData(500, 200, nameof(KnownResamplers.Lanczos8))] |
|||
[InlineData(100, 80, nameof(KnownResamplers.Lanczos8))] |
|||
[InlineData(100, 10, nameof(KnownResamplers.Lanczos8))] |
|||
[InlineData(10, 100, nameof(KnownResamplers.Lanczos8))] |
|||
public void PrintKernelMap(int srcSize, int destSize, string resamplerName) |
|||
{ |
|||
var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); |
|||
|
|||
var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); |
|||
|
|||
var bld = new StringBuilder(); |
|||
|
|||
foreach (ResizeKernel window in kernelMap.Kernels) |
|||
{ |
|||
Span<float> span = window.GetSpan(); |
|||
for (int i = 0; i < window.Length; i++) |
|||
{ |
|||
float value = span[i]; |
|||
bld.Append($"{value,7:F4}"); |
|||
bld.Append("| "); |
|||
} |
|||
|
|||
bld.AppendLine(); |
|||
} |
|||
|
|||
string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap)); |
|||
string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD"; |
|||
|
|||
File.WriteAllText(fileName, bld.ToString()); |
|||
|
|||
this.Output.WriteLine(bld.ToString()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
|||
{ |
|||
public partial class ResizeKernelMapTests |
|||
{ |
|||
/// <summary>
|
|||
/// Simplified reference implementation for <see cref="ResizeKernelMap"/> functionality.
|
|||
/// </summary>
|
|||
internal class ReferenceKernelMap |
|||
{ |
|||
private readonly ReferenceKernel[] kernels; |
|||
|
|||
public ReferenceKernelMap(ReferenceKernel[] kernels) |
|||
{ |
|||
this.kernels = kernels; |
|||
} |
|||
|
|||
public int DestinationSize => this.kernels.Length; |
|||
|
|||
public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex]; |
|||
|
|||
public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true) |
|||
{ |
|||
double ratio = (double)sourceSize / destinationSize; |
|||
double scale = ratio; |
|||
|
|||
if (scale < 1F) |
|||
{ |
|||
scale = 1F; |
|||
} |
|||
|
|||
TolerantMath tolerantMath = TolerantMath.Default; |
|||
|
|||
double radius = tolerantMath.Ceiling(scale * sampler.Radius); |
|||
|
|||
var result = new List<ReferenceKernel>(); |
|||
|
|||
for (int i = 0; i < destinationSize; i++) |
|||
{ |
|||
double center = ((i + .5) * ratio) - .5; |
|||
|
|||
// Keep inside bounds.
|
|||
int left = (int)tolerantMath.Ceiling(center - radius); |
|||
if (left < 0) |
|||
{ |
|||
left = 0; |
|||
} |
|||
|
|||
int right = (int)tolerantMath.Floor(center + radius); |
|||
if (right > sourceSize - 1) |
|||
{ |
|||
right = sourceSize - 1; |
|||
} |
|||
|
|||
double sum = 0; |
|||
|
|||
double[] values = new double[right - left + 1]; |
|||
|
|||
for (int j = left; j <= right; j++) |
|||
{ |
|||
double weight = sampler.GetValue((float)((j - center) / scale)); |
|||
sum += weight; |
|||
|
|||
values[j - left] = weight; |
|||
} |
|||
|
|||
if (sum > 0 && normalize) |
|||
{ |
|||
for (int w = 0; w < values.Length; w++) |
|||
{ |
|||
values[w] /= sum; |
|||
} |
|||
} |
|||
|
|||
float[] floatVals = values.Select(v => (float)v).ToArray(); |
|||
|
|||
result.Add(new ReferenceKernel(left, floatVals)); |
|||
} |
|||
|
|||
return new ReferenceKernelMap(result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
internal struct ReferenceKernel |
|||
{ |
|||
public ReferenceKernel(int left, float[] values) |
|||
{ |
|||
this.Left = left; |
|||
this.Values = values; |
|||
} |
|||
|
|||
public int Left { get; } |
|||
|
|||
public float[] Values { get; } |
|||
|
|||
public int Length => this.Values.Length; |
|||
|
|||
public static implicit operator ReferenceKernel(ResizeKernel orig) |
|||
{ |
|||
return new ReferenceKernel(orig.Left, orig.Values.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
|
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
|||
{ |
|||
public partial class ResizeKernelMapTests |
|||
{ |
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
public ResizeKernelMapTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// resamplerName, srcSize, destSize
|
|||
/// </summary>
|
|||
public static readonly TheoryData<string, int, int> KernelMapData = new TheoryData<string, int, int> |
|||
{ |
|||
{ nameof(KnownResamplers.Bicubic), 15, 10 }, |
|||
{ nameof(KnownResamplers.Bicubic), 10, 15 }, |
|||
{ nameof(KnownResamplers.Bicubic), 20, 20 }, |
|||
{ nameof(KnownResamplers.Bicubic), 50, 40 }, |
|||
{ nameof(KnownResamplers.Bicubic), 40, 50 }, |
|||
{ nameof(KnownResamplers.Bicubic), 500, 200 }, |
|||
{ nameof(KnownResamplers.Bicubic), 200, 500 }, |
|||
|
|||
{ nameof(KnownResamplers.Bicubic), 10, 25 }, |
|||
|
|||
{ nameof(KnownResamplers.Lanczos3), 16, 12 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 12, 16 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 12, 9 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 9, 12 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 6, 8 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 8, 6 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 20, 12 }, |
|||
|
|||
{ nameof(KnownResamplers.Lanczos3), 5, 25 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 5, 50 }, |
|||
|
|||
{ nameof(KnownResamplers.Lanczos3), 25, 5 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 50, 5 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 49, 5 }, |
|||
{ nameof(KnownResamplers.Lanczos3), 31, 5 }, |
|||
|
|||
{ nameof(KnownResamplers.Lanczos8), 500, 200 }, |
|||
{ nameof(KnownResamplers.Lanczos8), 100, 10 }, |
|||
{ nameof(KnownResamplers.Lanczos8), 100, 80 }, |
|||
{ nameof(KnownResamplers.Lanczos8), 10, 100 }, |
|||
|
|||
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
|
|||
{ nameof(KnownResamplers.Box), 378, 149 }, |
|||
{ nameof(KnownResamplers.Box), 349, 174 }, |
|||
|
|||
// Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
|
|||
{ nameof(KnownResamplers.Box), 201, 100 }, |
|||
{ nameof(KnownResamplers.Box), 199, 99 }, |
|||
{ nameof(KnownResamplers.Box), 10, 299 }, |
|||
{ nameof(KnownResamplers.Box), 299, 10 }, |
|||
{ nameof(KnownResamplers.Box), 301, 300 }, |
|||
{ nameof(KnownResamplers.Box), 1180, 480 }, |
|||
|
|||
{ nameof(KnownResamplers.Lanczos2), 3264, 3032 }, |
|||
|
|||
{ nameof(KnownResamplers.Bicubic), 1280, 2240 }, |
|||
{ nameof(KnownResamplers.Bicubic), 1920, 1680 }, |
|||
{ nameof(KnownResamplers.Bicubic), 3072, 2240 }, |
|||
|
|||
{ nameof(KnownResamplers.Welch), 300, 2008 }, |
|||
|
|||
// ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
|
|||
{ nameof(KnownResamplers.Bicubic), 10, 50 }, |
|||
{ nameof(KnownResamplers.Bicubic), 49, 301 }, |
|||
{ nameof(KnownResamplers.Bicubic), 301, 49 }, |
|||
{ nameof(KnownResamplers.Bicubic), 1680, 1200 }, |
|||
{ nameof(KnownResamplers.Box), 13, 299 }, |
|||
{ nameof(KnownResamplers.Lanczos5), 3032, 600 }, |
|||
}; |
|||
|
|||
public static TheoryData<string, int, int> GeneratedImageResizeData = |
|||
GenerateImageResizeData(); |
|||
|
|||
|
|||
[Theory(Skip = "Only for debugging and development")] |
|||
[MemberData(nameof(KernelMapData))] |
|||
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) |
|||
{ |
|||
IResampler resampler = TestUtils.GetResampler(resamplerName); |
|||
|
|||
var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false); |
|||
|
|||
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(KernelMapData))] |
|||
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) |
|||
{ |
|||
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); |
|||
} |
|||
|
|||
// Comprehensive but expensive tests, for ResizeKernelMap.
|
|||
// Enabling them can kill you, but sometimes you have to wear the burden!
|
|||
// AppVeyor will never follow you to these shadows of Mordor.
|
|||
#if false
|
|||
[Theory] |
|||
[MemberData(nameof(GeneratedImageResizeData))] |
|||
public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) |
|||
{ |
|||
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); |
|||
} |
|||
#endif
|
|||
|
|||
private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) |
|||
{ |
|||
IResampler resampler = TestUtils.GetResampler(resamplerName); |
|||
|
|||
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); |
|||
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); |
|||
|
|||
#if DEBUG
|
|||
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); |
|||
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); |
|||
#endif
|
|||
var comparer = new ApproximateFloatComparer(1e-6f); |
|||
|
|||
for (int i = 0; i < kernelMap.DestinationLength; i++) |
|||
{ |
|||
ResizeKernel kernel = kernelMap.GetKernel(i); |
|||
|
|||
ReferenceKernel referenceKernel = referenceMap.GetKernel(i); |
|||
|
|||
Assert.True( |
|||
referenceKernel.Length == kernel.Length, |
|||
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}"); |
|||
Assert.True( |
|||
referenceKernel.Left == kernel.Left, |
|||
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}"); |
|||
float[] expectedValues = referenceKernel.Values; |
|||
Span<float> actualValues = kernel.Values; |
|||
|
|||
Assert.Equal(expectedValues.Length, actualValues.Length); |
|||
|
|||
|
|||
|
|||
for (int x = 0; x < expectedValues.Length; x++) |
|||
{ |
|||
Assert.True( |
|||
comparer.Equals(expectedValues[x], actualValues[x]), |
|||
$"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static string PrintKernelMap(ResizeKernelMap kernelMap) => |
|||
PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i)); |
|||
|
|||
private static string PrintKernelMap(ReferenceKernelMap kernelMap) => |
|||
PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); |
|||
|
|||
private static string PrintKernelMap<TKernelMap>( |
|||
TKernelMap kernelMap, |
|||
Func<TKernelMap, int> getDestinationSize, |
|||
Func<TKernelMap, int, ReferenceKernel> getKernel) |
|||
{ |
|||
var bld = new StringBuilder(); |
|||
|
|||
if (kernelMap is ResizeKernelMap actualMap) |
|||
{ |
|||
bld.AppendLine(actualMap.Info); |
|||
} |
|||
|
|||
int destinationSize = getDestinationSize(kernelMap); |
|||
|
|||
for (int i = 0; i < destinationSize; i++) |
|||
{ |
|||
ReferenceKernel kernel = getKernel(kernelMap, i); |
|||
bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || "); |
|||
Span<float> span = kernel.Values; |
|||
|
|||
for (int j = 0; j < kernel.Length; j++) |
|||
{ |
|||
float value = span[j]; |
|||
bld.Append($"{value,8:F5}"); |
|||
bld.Append(" | "); |
|||
} |
|||
|
|||
bld.AppendLine(); |
|||
} |
|||
|
|||
return bld.ToString(); |
|||
} |
|||
|
|||
|
|||
private static TheoryData<string, int, int> GenerateImageResizeData() |
|||
{ |
|||
var result = new TheoryData<string, int, int>(); |
|||
|
|||
string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static) |
|||
.Select(p => p.Name) |
|||
.Where(name => name != nameof(KnownResamplers.NearestNeighbor)) |
|||
.ToArray(); |
|||
|
|||
int[] dimensionVals = |
|||
{ |
|||
// Arbitrary, small dimensions:
|
|||
9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, |
|||
|
|||
// Typical image sizes:
|
|||
640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, |
|||
1920, 3032, 2008, 3072, 2304, 3264, 2448 |
|||
}; |
|||
|
|||
IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals |
|||
.SelectMany(s => dimensionVals.Select(d => (s, d))) |
|||
.OrderBy(x => x.s + x.d); |
|||
|
|||
foreach (string resampler in resamplerNames) |
|||
{ |
|||
foreach ((int s, int d) x in source2Dest) |
|||
{ |
|||
result.Add(resampler, x.s, x.d); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Transforms |
|||
{ |
|||
public class AffineTransformBuilderTests : TransformBuilderTestBase<AffineTransformBuilder> |
|||
{ |
|||
protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); |
|||
|
|||
protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) |
|||
=> builder.AppendRotationDegrees(degrees); |
|||
|
|||
protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) |
|||
=> builder.AppendRotationDegrees(degrees, origin); |
|||
|
|||
protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) |
|||
=> builder.AppendRotationRadians(radians); |
|||
|
|||
protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) |
|||
=> builder.AppendRotationRadians(radians, origin); |
|||
|
|||
protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) |
|||
=> builder.AppendScale(scale); |
|||
|
|||
protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY) |
|||
=> builder.AppendSkewDegrees(degreesX, degreesY); |
|||
|
|||
protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) |
|||
=> builder.AppendSkewDegrees(degreesX, degreesY, origin); |
|||
|
|||
protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) |
|||
=> builder.AppendSkewRadians(radiansX, radiansY); |
|||
|
|||
protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) |
|||
=> builder.AppendSkewRadians(radiansX, radiansY, origin); |
|||
|
|||
protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) |
|||
=> builder.AppendTranslation(translate); |
|||
|
|||
protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) |
|||
=> builder.PrependRotationRadians(radians); |
|||
|
|||
protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) |
|||
=> builder.PrependRotationRadians(radians, origin); |
|||
|
|||
protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) |
|||
=> builder.PrependScale(scale); |
|||
|
|||
protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) |
|||
=> builder.PrependSkewRadians(radiansX, radiansY); |
|||
|
|||
protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) |
|||
=> builder.PrependSkewRadians(radiansX, radiansY, origin); |
|||
|
|||
protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) |
|||
=> builder.PrependTranslation(translate); |
|||
|
|||
protected override Vector2 Execute( |
|||
AffineTransformBuilder builder, |
|||
Rectangle rectangle, |
|||
Vector2 sourcePoint) |
|||
{ |
|||
Matrix3x2 matrix = builder.BuildMatrix(rectangle); |
|||
return Vector2.Transform(sourcePoint, matrix); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Transforms |
|||
{ |
|||
public class ProjectiveTransformBuilderTests : TransformBuilderTestBase<ProjectiveTransformBuilder> |
|||
{ |
|||
protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(); |
|||
|
|||
protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); |
|||
|
|||
protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); |
|||
|
|||
protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); |
|||
|
|||
protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); |
|||
|
|||
protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); |
|||
|
|||
protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY) |
|||
=> builder.AppendSkewDegrees(degreesX, degreesY); |
|||
|
|||
protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) |
|||
=> builder.AppendSkewDegrees(degreesX, degreesY, origin); |
|||
|
|||
protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) |
|||
=> builder.AppendSkewRadians(radiansX, radiansY); |
|||
|
|||
protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) |
|||
=> builder.AppendSkewRadians(radiansX, radiansY, origin); |
|||
|
|||
protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); |
|||
|
|||
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); |
|||
|
|||
protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); |
|||
|
|||
protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) |
|||
=> builder.PrependSkewRadians(radiansX, radiansY); |
|||
|
|||
protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) |
|||
=> builder.PrependSkewRadians(radiansX, radiansY, origin); |
|||
|
|||
protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); |
|||
|
|||
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => |
|||
builder.PrependRotationRadians(radians, origin); |
|||
|
|||
protected override Vector2 Execute( |
|||
ProjectiveTransformBuilder builder, |
|||
Rectangle rectangle, |
|||
Vector2 sourcePoint) |
|||
{ |
|||
Matrix4x4 matrix = builder.BuildMatrix(rectangle); |
|||
return Vector2.Transform(sourcePoint, matrix); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,275 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|||
using SixLabors.Primitives; |
|||
|
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Transforms |
|||
{ |
|||
public abstract class TransformBuilderTestBase<TBuilder> |
|||
{ |
|||
private static readonly ApproximateFloatComparer Comparer = new ApproximateFloatComparer(1e-6f); |
|||
|
|||
public static readonly TheoryData<Vector2, Vector2, Vector2, Vector2> ScaleTranslate_Data = |
|||
new TheoryData<Vector2, Vector2, Vector2, Vector2> |
|||
{ |
|||
// scale, translate, source, expectedDest
|
|||
{ Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero }, |
|||
{ Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) }, |
|||
{ Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) }, |
|||
{ new Vector2(2, 0.5f), new Vector2(3, 1), new Vector2(10, 20), new Vector2(23, 11) }, |
|||
}; |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ScaleTranslate_Data))] |
|||
public void _1Scale_2Translate(Vector2 scale, Vector2 translate, Vector2 source, Vector2 expectedDest) |
|||
{ |
|||
// These operations should be size-agnostic:
|
|||
var size = new Size(123, 321); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
this.AppendScale(builder, new SizeF(scale)); |
|||
this.AppendTranslation(builder, translate); |
|||
|
|||
Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); |
|||
Assert.True(Comparer.Equals(expectedDest, actualDest)); |
|||
} |
|||
|
|||
public static readonly TheoryData<Vector2, Vector2, Vector2, Vector2> TranslateScale_Data = |
|||
new TheoryData<Vector2, Vector2, Vector2, Vector2> |
|||
{ |
|||
// translate, scale, source, expectedDest
|
|||
{ Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero }, |
|||
{ Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) }, |
|||
{ new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) }, |
|||
}; |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(TranslateScale_Data))] |
|||
public void _1Translate_2Scale(Vector2 translate, Vector2 scale, Vector2 source, Vector2 expectedDest) |
|||
{ |
|||
// Translate ans scale are size-agnostic:
|
|||
var size = new Size(456, 432); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
this.AppendTranslation(builder, translate); |
|||
this.AppendScale(builder, new SizeF(scale)); |
|||
|
|||
Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); |
|||
Assert.Equal(expectedDest, actualDest, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(10, 20)] |
|||
[InlineData(-20, 10)] |
|||
public void LocationOffsetIsPrepended(int locationX, int locationY) |
|||
{ |
|||
var rectangle = new Rectangle(locationX, locationY, 10, 10); |
|||
TBuilder builder = this.CreateBuilder(rectangle); |
|||
|
|||
this.AppendScale(builder, new SizeF(2, 2)); |
|||
|
|||
Vector2 actual = this.Execute(builder, rectangle, Vector2.One); |
|||
Vector2 expected = new Vector2(-locationX + 1, -locationY + 1) * 2; |
|||
|
|||
Assert.Equal(actual, expected, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(200, 100, 10, 42, 84)] |
|||
[InlineData(200, 100, 100, 42, 84)] |
|||
[InlineData(100, 200, -10, 42, 84)] |
|||
public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter( |
|||
int width, |
|||
int height, |
|||
float degrees, |
|||
float x, |
|||
float y) |
|||
{ |
|||
var size = new Size(width, height); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
this.AppendRotationDegrees(builder, degrees); |
|||
|
|||
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
|
|||
Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(degrees, size); |
|||
|
|||
var position = new Vector2(x, y); |
|||
var expected = Vector2.Transform(position, matrix); |
|||
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); |
|||
|
|||
Assert.Equal(actual, expected, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(200, 100, 10, 30, 61, 42, 84)] |
|||
[InlineData(200, 100, 100, 30, 10, 20, 84)] |
|||
[InlineData(100, 200, -10, 30, 20, 11, 84)] |
|||
public void AppendRotationDegrees_WithRotationCenter( |
|||
int width, |
|||
int height, |
|||
float degrees, |
|||
float cx, |
|||
float cy, |
|||
float x, |
|||
float y) |
|||
{ |
|||
var size = new Size(width, height); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
var centerPoint = new Vector2(cx, cy); |
|||
this.AppendRotationDegrees(builder, degrees, centerPoint); |
|||
|
|||
var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint); |
|||
|
|||
var position = new Vector2(x, y); |
|||
var expected = Vector2.Transform(position, matrix); |
|||
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); |
|||
|
|||
Assert.Equal(actual, expected, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(200, 100, 10, 10, 42, 84)] |
|||
[InlineData(200, 100, 100, 100, 42, 84)] |
|||
[InlineData(100, 200, -10, -10, 42, 84)] |
|||
public void AppendSkewDegrees_WithoutSpecificSkewCenter_SkewIsCenteredAroundImageCenter( |
|||
int width, |
|||
int height, |
|||
float degreesX, |
|||
float degreesY, |
|||
float x, |
|||
float y) |
|||
{ |
|||
var size = new Size(width, height); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
this.AppendSkewDegrees(builder, degreesX, degreesY); |
|||
|
|||
Matrix3x2 matrix = TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size); |
|||
|
|||
var position = new Vector2(x, y); |
|||
var expected = Vector2.Transform(position, matrix); |
|||
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); |
|||
Assert.Equal(actual, expected, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(200, 100, 10, 10, 30, 61, 42, 84)] |
|||
[InlineData(200, 100, 100, 100, 30, 10, 20, 84)] |
|||
[InlineData(100, 200, -10, -10, 30, 20, 11, 84)] |
|||
public void AppendSkewDegrees_WithSkewCenter( |
|||
int width, |
|||
int height, |
|||
float degreesX, |
|||
float degreesY, |
|||
float cx, |
|||
float cy, |
|||
float x, |
|||
float y) |
|||
{ |
|||
var size = new Size(width, height); |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
|
|||
var centerPoint = new Vector2(cx, cy); |
|||
this.AppendSkewDegrees(builder, degreesX, degreesY, centerPoint); |
|||
|
|||
var matrix = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint); |
|||
|
|||
var position = new Vector2(x, y); |
|||
var expected = Vector2.Transform(position, matrix); |
|||
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); |
|||
|
|||
Assert.Equal(actual, expected, Comparer); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AppendPrependOpposite() |
|||
{ |
|||
var rectangle = new Rectangle(-1, -1, 3, 3); |
|||
TBuilder b1 = this.CreateBuilder(rectangle); |
|||
TBuilder b2 = this.CreateBuilder(rectangle); |
|||
|
|||
const float pi = (float)Math.PI; |
|||
|
|||
// Forwards
|
|||
this.AppendRotationRadians(b1, pi); |
|||
this.AppendSkewRadians(b1, pi, pi); |
|||
this.AppendScale(b1, new SizeF(2, 0.5f)); |
|||
this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f)); |
|||
this.AppendSkewRadians(b1, pi, pi / 2, new Vector2(-0.5f, -0.1f)); |
|||
this.AppendTranslation(b1, new PointF(123, 321)); |
|||
|
|||
// Backwards
|
|||
this.PrependTranslation(b2, new PointF(123, 321)); |
|||
this.PrependSkewRadians(b2, pi, pi / 2, new Vector2(-0.5f, -0.1f)); |
|||
this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f)); |
|||
this.PrependScale(b2, new SizeF(2, 0.5f)); |
|||
this.PrependSkewRadians(b2, pi, pi); |
|||
this.PrependRotationRadians(b2, pi); |
|||
|
|||
Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65)); |
|||
Vector2 p2 = this.Execute(b2, rectangle, new Vector2(32, 65)); |
|||
|
|||
Assert.Equal(p1, p2, Comparer); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0, 1)] |
|||
[InlineData(1, 0)] |
|||
[InlineData(-1, 0)] |
|||
public void ThrowsForInvalidSizes(int width, int height) |
|||
{ |
|||
var size = new Size(width, height); |
|||
|
|||
Assert.ThrowsAny<ArgumentOutOfRangeException>( |
|||
() => |
|||
{ |
|||
TBuilder builder = this.CreateBuilder(size); |
|||
this.Execute(builder, new Rectangle(Point.Empty, size), Vector2.Zero); |
|||
}); |
|||
} |
|||
|
|||
protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size)); |
|||
|
|||
protected abstract TBuilder CreateBuilder(Rectangle rectangle); |
|||
|
|||
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); |
|||
|
|||
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); |
|||
|
|||
protected abstract void AppendRotationRadians(TBuilder builder, float radians); |
|||
|
|||
protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin); |
|||
|
|||
protected abstract void AppendScale(TBuilder builder, SizeF scale); |
|||
|
|||
protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY); |
|||
|
|||
protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY, Vector2 origin); |
|||
|
|||
protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY); |
|||
|
|||
protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); |
|||
|
|||
protected abstract void AppendTranslation(TBuilder builder, PointF translate); |
|||
|
|||
protected abstract void PrependRotationRadians(TBuilder builder, float radians); |
|||
|
|||
protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); |
|||
|
|||
protected abstract void PrependScale(TBuilder builder, SizeF scale); |
|||
|
|||
protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY); |
|||
|
|||
protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); |
|||
|
|||
protected abstract void PrependTranslation(TBuilder builder, PointF translate); |
|||
|
|||
protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit ed8a7b0b6fe1b2e2a7c822aa617103ae31192655 |
|||
Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128 |
|||
|
After Width: | Height: | Size: 265 B |
|
After Width: | Height: | Size: 267 B |
|
After Width: | Height: | Size: 267 B |
Loading…
Reference in new issue