Skip to content

Commit 24576ef

Browse files
Merge pull request #3011 from SixLabors/js/fix-3000
Fix off-by-one errors when transforming images.
2 parents fd8331f + 726794a commit 24576ef

File tree

113 files changed

+550
-470
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+550
-470
lines changed

src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ internal void SyncSubject(int width, int height, Matrix4x4 matrix)
318318
{
319319
if (location.Value?.Length == 2)
320320
{
321-
Vector2 point = TransformUtils.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
321+
Vector2 point = TransformUtilities.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
322322

323323
// Ensure the point is within the image dimensions.
324324
point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1));
@@ -340,18 +340,18 @@ internal void SyncSubject(int width, int height, Matrix4x4 matrix)
340340
if (area.Value?.Length == 4)
341341
{
342342
RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
343-
if (!TransformUtils.TryGetTransformedRectangle(rectangle, matrix, out Rectangle bounds))
343+
if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds))
344344
{
345345
return;
346346
}
347347

348348
// Ensure the bounds are within the image dimensions.
349-
bounds = Rectangle.Intersect(bounds, new Rectangle(0, 0, width, height));
349+
bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, height));
350350

351-
area.Value[0] = (ushort)bounds.X;
352-
area.Value[1] = (ushort)bounds.Y;
353-
area.Value[2] = (ushort)bounds.Width;
354-
area.Value[3] = (ushort)bounds.Height;
351+
area.Value[0] = (ushort)MathF.Floor(bounds.X);
352+
area.Value[1] = (ushort)MathF.Floor(bounds.Y);
353+
area.Value[2] = (ushort)MathF.Ceiling(bounds.Width);
354+
area.Value[3] = (ushort)MathF.Ceiling(bounds.Height);
355355
this.SetValue(ExifTag.SubjectArea, area.Value);
356356
}
357357
else

src/ImageSharp/Primitives/Point.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public Point(Size size)
6969
/// Gets a value indicating whether this <see cref="Point"/> is empty.
7070
/// </summary>
7171
[EditorBrowsable(EditorBrowsableState.Never)]
72-
public bool IsEmpty => this.Equals(Empty);
72+
public readonly bool IsEmpty => this.Equals(Empty);
7373

7474
/// <summary>
7575
/// Creates a <see cref="PointF"/> with the coordinates of the specified <see cref="Point"/>.
@@ -239,7 +239,7 @@ public Point(Size size)
239239
/// </summary>
240240
/// <param name="x">The out value for X.</param>
241241
/// <param name="y">The out value for Y.</param>
242-
public void Deconstruct(out int x, out int y)
242+
public readonly void Deconstruct(out int x, out int y)
243243
{
244244
x = this.X;
245245
y = this.Y;
@@ -268,17 +268,17 @@ public void Offset(int dx, int dy)
268268
public void Offset(Point point) => this.Offset(point.X, point.Y);
269269

270270
/// <inheritdoc/>
271-
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
271+
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
272272

273273
/// <inheritdoc/>
274-
public override string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
274+
public override readonly string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
275275

276276
/// <inheritdoc/>
277-
public override bool Equals(object? obj) => obj is Point other && this.Equals(other);
277+
public override readonly bool Equals(object? obj) => obj is Point other && this.Equals(other);
278278

279279
/// <inheritdoc/>
280280
[MethodImpl(MethodImplOptions.AggressiveInlining)]
281-
public bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
281+
public readonly bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
282282

283283
private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff));
284284

src/ImageSharp/Primitives/PointF.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public PointF(SizeF size)
5858
/// Gets a value indicating whether this <see cref="PointF"/> is empty.
5959
/// </summary>
6060
[EditorBrowsable(EditorBrowsableState.Never)]
61-
public bool IsEmpty => this.Equals(Empty);
61+
public readonly bool IsEmpty => this.Equals(Empty);
6262

6363
/// <summary>
6464
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@@ -251,7 +251,7 @@ public PointF(SizeF size)
251251
/// </summary>
252252
/// <param name="x">The out value for X.</param>
253253
/// <param name="y">The out value for Y.</param>
254-
public void Deconstruct(out float x, out float y)
254+
public readonly void Deconstruct(out float x, out float y)
255255
{
256256
x = this.X;
257257
y = this.Y;
@@ -277,15 +277,15 @@ public void Offset(float dx, float dy)
277277
public void Offset(PointF point) => this.Offset(point.X, point.Y);
278278

279279
/// <inheritdoc/>
280-
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
280+
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
281281

282282
/// <inheritdoc/>
283-
public override string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
283+
public override readonly string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
284284

285285
/// <inheritdoc/>
286-
public override bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF);
286+
public override readonly bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF);
287287

288288
/// <inheritdoc/>
289289
[MethodImpl(MethodImplOptions.AggressiveInlining)]
290-
public bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
290+
public readonly bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
291291
}

src/ImageSharp/Primitives/SizeF.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public SizeF(PointF point)
6767
/// Gets a value indicating whether this <see cref="SizeF"/> is empty.
6868
/// </summary>
6969
[EditorBrowsable(EditorBrowsableState.Never)]
70-
public bool IsEmpty => this.Equals(Empty);
70+
public readonly bool IsEmpty => this.Equals(Empty);
7171

7272
/// <summary>
7373
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@@ -201,24 +201,24 @@ public static SizeF Transform(SizeF size, Matrix3x2 matrix)
201201
/// </summary>
202202
/// <param name="width">The out value for the width.</param>
203203
/// <param name="height">The out value for the height.</param>
204-
public void Deconstruct(out float width, out float height)
204+
public readonly void Deconstruct(out float width, out float height)
205205
{
206206
width = this.Width;
207207
height = this.Height;
208208
}
209209

210210
/// <inheritdoc/>
211-
public override int GetHashCode() => HashCode.Combine(this.Width, this.Height);
211+
public override readonly int GetHashCode() => HashCode.Combine(this.Width, this.Height);
212212

213213
/// <inheritdoc/>
214-
public override string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
214+
public override readonly string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
215215

216216
/// <inheritdoc/>
217-
public override bool Equals(object? obj) => obj is SizeF && this.Equals((SizeF)obj);
217+
public override readonly bool Equals(object? obj) => obj is SizeF sizeF && this.Equals(sizeF);
218218

219219
/// <inheritdoc/>
220220
[MethodImpl(MethodImplOptions.AggressiveInlining)]
221-
public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
221+
public readonly bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
222222

223223
/// <summary>
224224
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.

src/ImageSharp/Processing/AffineTransformBuilder.cs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,9 @@ public class AffineTransformBuilder
1717
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
1818
/// </summary>
1919
public AffineTransformBuilder()
20-
: this(TransformSpace.Pixel)
2120
{
2221
}
2322

24-
/// <summary>
25-
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
26-
/// </summary>
27-
/// <param name="transformSpace">
28-
/// The <see cref="TransformSpace"/> to use when applying the affine transform.
29-
/// </param>
30-
public AffineTransformBuilder(TransformSpace transformSpace)
31-
=> this.TransformSpace = transformSpace;
32-
33-
/// <summary>
34-
/// Gets the <see cref="TransformSpace"/> to use when applying the affine transform.
35-
/// </summary>
36-
public TransformSpace TransformSpace { get; }
37-
3823
/// <summary>
3924
/// Prepends a rotation matrix using the given rotation angle in degrees
4025
/// and the image center point as rotation center.
@@ -52,7 +37,7 @@ public AffineTransformBuilder PrependRotationDegrees(float degrees)
5237
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
5338
public AffineTransformBuilder PrependRotationRadians(float radians)
5439
=> this.Prepend(
55-
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
40+
size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
5641

5742
/// <summary>
5843
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@@ -88,7 +73,7 @@ public AffineTransformBuilder AppendRotationDegrees(float degrees)
8873
/// <param name="radians">The amount of rotation, in radians.</param>
8974
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
9075
public AffineTransformBuilder AppendRotationRadians(float radians)
91-
=> this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
76+
=> this.Append(size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
9277

9378
/// <summary>
9479
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
@@ -172,7 +157,7 @@ public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
172157
/// <param name="radiansY">The Y angle, in radians.</param>
173158
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
174159
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
175-
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
160+
=> this.Prepend(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
176161

177162
/// <summary>
178163
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@@ -210,7 +195,7 @@ public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
210195
/// <param name="radiansY">The Y angle, in radians.</param>
211196
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
212197
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
213-
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
198+
=> this.Append(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
214199

215200
/// <summary>
216201
/// Appends a skew matrix using the given angles in degrees at the given origin.
@@ -344,15 +329,29 @@ public Matrix3x2 BuildMatrix(Rectangle sourceRectangle)
344329
/// for linear transforms.
345330
/// </exception>
346331
/// <returns>The <see cref="Size"/>.</returns>
347-
public Size GetTransformedSize(Rectangle sourceRectangle)
332+
public SizeF GetTransformedSize(Rectangle sourceRectangle)
348333
{
349334
Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
350-
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
335+
return GetTransformedSize(sourceRectangle, matrix);
351336
}
352337

338+
/// <summary>
339+
/// Returns the size of a rectangle large enough to contain the transformed source rectangle.
340+
/// </summary>
341+
/// <param name="sourceRectangle">The rectangle in the source image.</param>
342+
/// <param name="matrix">The transformation matrix.</param>
343+
/// <exception cref="DegenerateTransformException">
344+
/// The resultant matrix is degenerate containing one or more values equivalent
345+
/// to <see cref="float.NaN"/> or a zero determinant and therefore cannot be used
346+
/// for linear transforms.
347+
/// </exception>
348+
/// <returns>The <see cref="Size"/>.</returns>
349+
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
350+
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
351+
353352
private static void CheckDegenerate(Matrix3x2 matrix)
354353
{
355-
if (TransformUtils.IsDegenerate(matrix))
354+
if (TransformUtilities.IsDegenerate(matrix))
356355
{
357356
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
358357
}

src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
namespace SixLabors.ImageSharp.Processing;
88

99
/// <summary>
10-
/// Defines extensions that allow the application of composable transform operations on an <see cref="Image"/>
11-
/// using Mutate/Clone.
10+
/// Defines extensions that allow the application of composable transform operations
11+
/// on an <see cref="IImageProcessingContext"/> using Mutate/Clone.
1212
/// </summary>
1313
public static class TransformExtensions
1414
{
@@ -51,7 +51,7 @@ public static IImageProcessingContext Transform(
5151
IResampler sampler)
5252
{
5353
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
54-
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
54+
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
5555
return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
5656
}
5757

@@ -113,7 +113,7 @@ public static IImageProcessingContext Transform(
113113
IResampler sampler)
114114
{
115115
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
116-
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
116+
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
117117
return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
118118
}
119119

src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targe
2121
Guard.NotNull(sampler, nameof(sampler));
2222
Guard.MustBeValueType(sampler);
2323

24-
if (TransformUtils.IsDegenerate(matrix))
24+
if (TransformUtilities.IsDegenerate(matrix))
2525
{
2626
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
2727
}

src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
7777
return;
7878
}
7979

80-
// Convert from screen to world space.
80+
// All matrices are defined in normalized coordinate space so we need to convert to pixel space.
81+
// After normalization we need to invert the matrix for correct sampling.
82+
matrix = TransformUtilities.NormalizeToPixel(matrix);
8183
Matrix3x2.Invert(matrix, out matrix);
8284

8385
if (sampler is NearestNeighborResampler)

src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size t
2121
Guard.NotNull(sampler, nameof(sampler));
2222
Guard.MustBeValueType(sampler);
2323

24-
if (TransformUtils.IsDegenerate(matrix))
24+
if (TransformUtilities.IsDegenerate(matrix))
2525
{
2626
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
2727
}

src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
7575
return;
7676
}
7777

78-
// Convert from screen to world space.
78+
// All matrices are defined in normalized coordinate space so we need to convert to pixel space.
79+
// After normalization we need to invert the matrix for correct sampling.
80+
matrix = TransformUtilities.NormalizeToPixel(matrix);
7981
Matrix4x4.Invert(matrix, out matrix);
8082

8183
if (sampler is NearestNeighborResampler)
@@ -135,7 +137,7 @@ public void Invoke(int y)
135137

136138
for (int x = 0; x < destinationRowSpan.Length; x++)
137139
{
138-
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
140+
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
139141
int px = (int)MathF.Round(point.X);
140142
int py = (int)MathF.Round(point.Y);
141143

@@ -207,7 +209,7 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)
207209

208210
for (int x = 0; x < span.Length; x++)
209211
{
210-
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix);
212+
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, matrix);
211213
float pY = point.Y;
212214
float pX = point.X;
213215

0 commit comments

Comments
 (0)