Browse Source

Update Subject EXIF metadata when transforming images.

pull/2946/head
James Jackson-South 8 months ago
parent
commit
668e223fa1
  1. 3
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  2. 3
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  3. 3
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  4. 3
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  5. 3
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  6. 4
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  7. 4
      src/ImageSharp/Formats/IFormatMetadata.cs
  8. 3
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  9. 3
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  10. 3
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  11. 3
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  12. 3
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  13. 3
      src/ImageSharp/Formats/Png/PngMetadata.cs
  14. 3
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  15. 3
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  16. 3
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  17. 3
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  18. 3
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  19. 3
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  20. 10
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  21. 7
      src/ImageSharp/Metadata/ImageMetadata.cs
  22. 69
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  23. 5
      src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
  24. 6
      src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs
  25. 18
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs
  26. 3
      src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs
  27. 4
      src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs
  28. 5
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  29. 42
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  30. 3
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  31. 34
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  32. 16
      src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs
  33. 5
      src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs
  34. 18
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
  35. 24
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  36. 32
      tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs
  37. 78
      tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs
  38. 127
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs
  39. 76
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  40. 56
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  41. 40
      tests/ImageSharp.Tests/Processing/Processors/Transforms/SwizzleTests.cs

3
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
// TODO: Add color table information.
@ -156,7 +157,7 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
public BmpMetadata DeepClone() => new(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null;
}

3
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -117,7 +118,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;

3
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -148,7 +149,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null;

3
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -103,7 +104,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
}
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.LocalColorTable = null;

3
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -105,7 +106,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.GlobalColorTable = null;

4
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@ -22,7 +23,8 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
/// <param name="matrix">The transformation matrix applied to the image frame.</param>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Formats/IFormatMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@ -27,7 +28,8 @@ public interface IFormatMetadata : IDeepCloneable
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
/// <param name="matrix">The transformation matrix applied to the image.</param>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>;
}

3
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -110,7 +111,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;

3
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -148,7 +149,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null;

3
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats;
@ -200,7 +201,7 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Pbm/PbmMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm;
@ -130,7 +131,7 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
@ -86,7 +87,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
}
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
@ -227,7 +228,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null;

3
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
@ -89,7 +90,7 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Tga/TgaMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tga;
@ -95,7 +96,7 @@ public class TgaMetadata : IFormatMetadata<TgaMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
@ -96,7 +97,7 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;

3
src/ImageSharp/Formats/Tiff/TiffMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
@ -181,7 +182,7 @@ public class TiffMetadata : IFormatMetadata<TiffMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -66,7 +67,7 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

3
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -146,7 +147,7 @@ public class WebpMetadata : IFormatMetadata<WebpMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

10
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -158,16 +159,21 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
internal void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
/// <param name="matrix">The transformation matrix applied to the frame.</param>
internal void AfterFrameApply<TPixel>(
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
// Always updated using the full frame dimensions.
// Individual format frame metadata will update with sub region dimensions if appropriate.
this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix);
foreach (KeyValuePair<IImageFormat, IFormatFrameMetadata> meta in this.formatMetadata)
{
meta.Value.AfterFrameApply(source, destination);
meta.Value.AfterFrameApply(source, destination, matrix);
}
}
}

7
src/ImageSharp/Metadata/ImageMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -235,14 +236,16 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image.</param>
internal void AfterImageApply<TPixel>(Image<TPixel> destination)
/// <param name="matrix">The transformation matrix applied to the image.</param>
internal void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix);
foreach (KeyValuePair<IImageFormat, IFormatMetadata> meta in this.formatMetadata)
{
meta.Value.AfterImageApply(destination);
meta.Value.AfterImageApply(destination, matrix);
}
}

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

@ -2,8 +2,9 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using static System.Runtime.InteropServices.JavaScript.JSType;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -48,7 +49,7 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
{
this.Parts = ExifParts.All;
this.data = data;
this.InvalidTags = Array.Empty<ExifTag>();
this.InvalidTags = [];
}
/// <summary>
@ -161,11 +162,9 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
return false;
}
using (MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength))
{
image = Image.Load<TPixel>(memStream);
return true;
}
using MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength);
image = Image.Load<TPixel>(memStream);
return true;
}
/// <summary>
@ -267,7 +266,7 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
/// </summary>
/// <param name="tag">The tag of the exif value.</param>
/// <param name="value">The value.</param>
/// <exception cref="NotSupportedException">Newly created value is null.</exception>
/// <exception cref="NotSupportedException">The newly created value is null.</exception>
internal void SetValueInternal(ExifTag tag, object? value)
{
foreach (IExifValue exifValue in this.Values)
@ -312,6 +311,60 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
}
}
internal void SyncSubject(int width, int height, Matrix4x4 matrix)
{
if (matrix.IsIdentity)
{
return;
}
if (this.TryGetValue(ExifTag.SubjectLocation, out IExifValue<ushort[]>? location))
{
if (location.Value?.Length == 2)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
// Ensure the point is within the image dimensions.
point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1));
// Round the point to the nearest pixel.
location.Value[0] = (ushort)Math.Floor(point.X);
location.Value[1] = (ushort)Math.Floor(point.Y);
this.SetValue(ExifTag.SubjectLocation, location.Value);
}
else
{
this.RemoveValue(ExifTag.SubjectLocation);
}
}
if (this.TryGetValue(ExifTag.SubjectArea, out IExifValue<ushort[]>? area))
{
if (area.Value?.Length == 4)
{
RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
if (!TransformUtils.TryGetTransformedRectangle(rectangle, matrix, out Rectangle bounds))
{
return;
}
// Ensure the bounds are within the image dimensions.
bounds = Rectangle.Intersect(bounds, new Rectangle(0, 0, width, height));
area.Value[0] = (ushort)bounds.X;
area.Value[1] = (ushort)bounds.Y;
area.Value[2] = (ushort)bounds.Width;
area.Value[3] = (ushort)bounds.Height;
this.SetValue(ExifTag.SubjectArea, area.Value);
}
else
{
this.RemoveValue(ExifTag.SubjectArea);
}
}
}
/// <summary>
/// Synchronizes the profiles with the specified metadata.
/// </summary>

5
src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors;
@ -132,14 +133,14 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
/// <param name="source">The source image. Cannot be null.</param>
/// <param name="destination">The cloned/destination image. Cannot be null.</param>
protected virtual void AfterFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
=> destination.Metadata.AfterFrameApply(source, destination);
=> destination.Metadata.AfterFrameApply(source, destination, Matrix4x4.Identity);
/// <summary>
/// This method is called after the process is applied to prepare the processor.
/// </summary>
/// <param name="destination">The cloned/destination image. Cannot be null.</param>
protected virtual void AfterImageApply(Image<TPixel> destination)
=> destination.Metadata.AfterImageApply(destination);
=> destination.Metadata.AfterImageApply(destination, Matrix4x4.Identity);
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.

6
src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors;
@ -99,14 +100,13 @@ public abstract class ImageProcessor<TPixel> : IImageProcessor<TPixel>
/// </summary>
/// <param name="source">The source image. Cannot be null.</param>
protected virtual void AfterFrameApply(ImageFrame<TPixel> source)
{
}
=> source.Metadata.AfterFrameApply(source, source, Matrix4x4.Identity);
/// <summary>
/// This method is called after the process is applied to the complete image.
/// </summary>
protected virtual void AfterImageApply()
=> this.Source.Metadata.AfterImageApply(this.Source);
=> this.Source.Metadata.AfterImageApply(this.Source, Matrix4x4.Identity);
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.

18
src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -16,6 +17,7 @@ internal class CropProcessor<TPixel> : TransformProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Rectangle cropRectangle;
private readonly Matrix4x4 transformMatrix;
/// <summary>
/// Initializes a new instance of the <see cref="CropProcessor{TPixel}"/> class.
@ -26,10 +28,20 @@ internal class CropProcessor<TPixel> : TransformProcessor<TPixel>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public CropProcessor(Configuration configuration, CropProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
=> this.cropRectangle = definition.CropRectangle;
{
this.cropRectangle = definition.CropRectangle;
// Calculate the transform matrix from the crop operation to allow us
// to update any metadata that represents pixel coordinates in the source image.
this.transformMatrix = new ProjectiveTransformBuilder()
.AppendTranslation(new PointF(-this.cropRectangle.X, -this.cropRectangle.Y))
.BuildMatrix(sourceRectangle);
}
/// <inheritdoc/>
protected override Size GetDestinationSize() => new Size(this.cropRectangle.Width, this.cropRectangle.Height);
protected override Size GetDestinationSize() => new(this.cropRectangle.Width, this.cropRectangle.Height);
protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
@ -50,7 +62,7 @@ internal class CropProcessor<TPixel> : TransformProcessor<TPixel>
ParallelExecutionSettings parallelSettings =
ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4);
var operation = new RowOperation(bounds, source.PixelBuffer, destination.PixelBuffer);
RowOperation operation = new(bounds, source.PixelBuffer, destination.PixelBuffer);
ParallelRowIterator.IterateRows(
bounds,

3
src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Binarization;
using SixLabors.ImageSharp.Processing.Processors.Convolution;
@ -36,7 +35,7 @@ internal class EntropyCropProcessor<TPixel> : ImageProcessor<TPixel>
// TODO: This is clunky. We should add behavior enum to ExtractFrame.
// All frames have be the same size so we only need to calculate the correct dimensions for the first frame
using (Image<TPixel> temp = new(this.Configuration, this.Source.Metadata.DeepClone(), new[] { this.Source.Frames.RootFrame.Clone() }))
using (Image<TPixel> temp = new(this.Configuration, this.Source.Metadata.DeepClone(), [this.Source.Frames.RootFrame.Clone()]))
{
Configuration configuration = this.Source.Configuration;

4
src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs

@ -11,12 +11,12 @@ public interface ISwizzler
/// <summary>
/// Gets the size of the image after transformation.
/// </summary>
Size DestinationSize { get; }
public Size DestinationSize { get; }
/// <summary>
/// Applies the swizzle transformation to a given point.
/// </summary>
/// <param name="point">Point to transform.</param>
/// <returns>The transformed point.</returns>
Point Transform(Point point);
public Point Transform(Point point);
}

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

@ -18,6 +18,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
{
private readonly Size destinationSize;
private readonly Matrix3x2 transformMatrix;
private readonly Matrix4x4 transformMatrix4x4;
private readonly IResampler resampler;
private ImageFrame<TPixel>? source;
private ImageFrame<TPixel>? destination;
@ -34,6 +35,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
{
this.destinationSize = definition.DestinationSize;
this.transformMatrix = definition.TransformMatrix;
this.transformMatrix4x4 = new(this.transformMatrix);
this.resampler = definition.Sampler;
}
@ -47,6 +49,9 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
this.resampler.ApplyTransform(this);
}
/// <inheritdoc/>
protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix4x4;
/// <inheritdoc/>
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -17,16 +18,45 @@ internal class FlipProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly FlipProcessor definition;
private readonly Matrix4x4 transformMatrix;
/// <summary>
/// Initializes a new instance of the <see cref="FlipProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="definition">The <see cref="FlipProcessor"/>.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public FlipProcessor(Configuration configuration, FlipProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) => this.definition = definition;
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
// Calculate the transform matrix from the flip operation to allow us
// to update any metadata that represents pixel coordinates in the source image.
ProjectiveTransformBuilder builder = new();
switch (this.definition.FlipMode)
{
// No default needed as we have already set the pixels.
case FlipMode.Vertical:
// Flip vertically by scaling the Y axis by -1 and translating the Y coordinate.
builder.AppendScale(new Vector2(1, -1))
.AppendTranslation(new PointF(0, this.SourceRectangle.Height - 1));
break;
case FlipMode.Horizontal:
// Flip horizontally by scaling the X axis by -1 and translating the X coordinate.
builder.AppendScale(new Vector2(-1, 1))
.AppendTranslation(new PointF(this.SourceRectangle.Width - 1, 0));
break;
default:
this.transformMatrix = Matrix4x4.Identity;
return;
}
this.transformMatrix = builder.BuildMatrix(sourceRectangle);
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
@ -43,6 +73,14 @@ internal class FlipProcessor<TPixel> : ImageProcessor<TPixel>
}
}
/// <inheritdoc/>
protected override void AfterFrameApply(ImageFrame<TPixel> source)
=> source.Metadata.AfterFrameApply(source, source, this.transformMatrix);
/// <inheritdoc/>
protected override void AfterImageApply()
=> this.Source.Metadata.AfterImageApply(this.Source, this.transformMatrix);
/// <summary>
/// Swaps the image at the X-axis, which goes horizontally through the middle at half the height of the image.
/// </summary>

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

@ -47,6 +47,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
this.resampler.ApplyTransform(this);
}
/// <inheritdoc/>
protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix;
/// <inheritdoc/>
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -21,6 +22,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
private readonly IResampler resampler;
private readonly Rectangle destinationRectangle;
private Image<TPixel>? destination;
private readonly Matrix4x4 transformMatrix;
public ResizeProcessor(Configuration configuration, ResizeProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
@ -30,6 +32,17 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
this.destinationRectangle = definition.DestinationRectangle;
this.options = definition.Options;
this.resampler = definition.Options.Sampler;
// Calculate the transform matrix from the resize operation to allow us
// to update any metadata that represents pixel coordinates in the source image.
Vector2 scale = new(
this.destinationRectangle.Width / (float)this.SourceRectangle.Width,
this.destinationRectangle.Height / (float)this.SourceRectangle.Height);
this.transformMatrix = new ProjectiveTransformBuilder()
.AppendScale(scale)
.AppendTranslation((PointF)this.destinationRectangle.Location)
.BuildMatrix(sourceRectangle);
}
/// <inheritdoc/>
@ -50,6 +63,9 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
// Everything happens in BeforeImageApply.
}
/// <inheritdoc/>
protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix;
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler
{
@ -81,7 +97,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
return;
}
var interest = Rectangle.Intersect(destinationRectangle, destination.Bounds);
Rectangle interest = Rectangle.Intersect(destinationRectangle, destination.Bounds);
if (sampler is NearestNeighborResampler)
{
@ -110,13 +126,13 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
// Since all image frame dimensions have to be the same we can calculate
// the kernel maps and reuse for all frames.
MemoryAllocator allocator = configuration.MemoryAllocator;
using var horizontalKernelMap = ResizeKernelMap.Calculate(
using ResizeKernelMap horizontalKernelMap = ResizeKernelMap.Calculate(
in sampler,
destinationRectangle.Width,
sourceRectangle.Width,
allocator);
using var verticalKernelMap = ResizeKernelMap.Calculate(
using ResizeKernelMap verticalKernelMap = ResizeKernelMap.Calculate(
in sampler,
destinationRectangle.Height,
sourceRectangle.Height,
@ -158,7 +174,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
float widthFactor = sourceRectangle.Width / (float)destinationRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height;
var operation = new NNRowOperation(
NNRowOperation operation = new(
sourceRectangle,
destinationRectangle,
interest,
@ -179,10 +195,8 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
{
return PixelConversionModifiers.Premultiply.ApplyCompanding(compand);
}
else
{
return PixelConversionModifiers.None.ApplyCompanding(compand);
}
return PixelConversionModifiers.None.ApplyCompanding(compand);
}
private static void ApplyResizeFrameTransform(
@ -208,7 +222,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
// To reintroduce parallel processing, we would launch multiple workers
// for different row intervals of the image.
using var worker = new ResizeWorker<TPixel>(
using ResizeWorker<TPixel> worker = new(
configuration,
sourceRegion,
conversionModifiers,
@ -218,7 +232,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
destinationRectangle.Location);
worker.Initialize();
var workingInterval = new RowInterval(interest.Top, interest.Bottom);
RowInterval workingInterval = new(interest.Top, interest.Bottom);
worker.FillDestinationPixels(workingInterval, destination.PixelBuffer);
}

16
src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -12,17 +13,28 @@ internal class SwizzleProcessor<TSwizzler, TPixel> : TransformProcessor<TPixel>
{
private readonly TSwizzler swizzler;
private readonly Size destinationSize;
private readonly Matrix4x4 transformMatrix;
public SwizzleProcessor(Configuration configuration, TSwizzler swizzler, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.swizzler = swizzler;
this.destinationSize = swizzler.DestinationSize;
// Calculate the transform matrix from the swizzle operation to allow us
// to update any metadata that represents pixel coordinates in the source image.
this.transformMatrix = new ProjectiveTransformBuilder()
.AppendMatrix(TransformUtils.GetSwizzlerMatrix(swizzler, sourceRectangle))
.BuildMatrix(sourceRectangle);
}
protected override Size GetDestinationSize()
=> this.destinationSize;
/// <inheritdoc />
protected override Size GetDestinationSize() => this.destinationSize;
/// <inheritdoc />
protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix;
/// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
Point p = default;

5
src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs

@ -16,10 +16,7 @@ public sealed class SwizzleProcessor<TSwizzler> : IImageProcessor
/// Initializes a new instance of the <see cref="SwizzleProcessor{TSwizzler}"/> class.
/// </summary>
/// <param name="swizzler">The swizzler operation.</param>
public SwizzleProcessor(TSwizzler swizzler)
{
this.Swizzler = swizzler;
}
public SwizzleProcessor(TSwizzler swizzler) => this.Swizzler = swizzler;
/// <summary>
/// Gets the swizzler operation.

18
src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs

@ -1,9 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
// TODO: DO we need this class?
namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
/// <summary>
@ -23,4 +23,20 @@ internal abstract class TransformProcessor<TPixel> : CloningImageProcessor<TPixe
: base(configuration, source, sourceRectangle)
{
}
/// <summary>
/// Gets the transform matrix that will be applied to the image.
/// </summary>
/// <returns>
/// The <see cref="Matrix4x4"/> that represents the transformation to be applied to the image.
/// </returns>
protected abstract Matrix4x4 GetTransformMatrix();
/// <inheritdoc/>
protected override void AfterFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
=> destination.Metadata.AfterFrameApply(source, destination, this.GetTransformMatrix());
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> destination)
=> destination.Metadata.AfterImageApply(destination, this.GetTransformMatrix());
}

24
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -419,6 +419,28 @@ internal static class TransformUtils
return size;
}
/// <summary>
/// Attempts to derive a 4x4 projective transform matrix that approximates the behavior of an <typeparamref name="T"/>.
/// </summary>
/// <param name="swizzler">
/// The swizzler to use for the transformation.
/// </param>
/// <param name="sourceRectangle">
/// The source rectangle that defines the area to be transformed.
/// </param>
/// <typeparam name="T">
/// The type of the swizzler, which must implement <see cref="ISwizzler"/>.
/// </typeparam>
public static Matrix4x4 GetSwizzlerMatrix<T>(T swizzler, Rectangle sourceRectangle)
where T : struct, ISwizzler
=> CreateQuadDistortionMatrix(
sourceRectangle,
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)),
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)),
TransformSpace.Pixel);
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
@ -504,7 +526,7 @@ internal static class TransformUtils
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
{
if (matrix.IsIdentity || rectangle.Equals(default))
{

32
tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs

@ -1,8 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
@ -17,12 +19,36 @@ public class CropTest
public void Crop<TPixel>(TestImageProvider<TPixel> provider, int x, int y, int w, int h)
where TPixel : unmanaged, IPixel<TPixel>
{
var rect = new Rectangle(x, y, w, h);
Rectangle rect = new(x, y, w, h);
FormattableString info = $"X{x}Y{y}.W{w}H{h}";
provider.RunValidatingProcessorTest(
ctx => ctx.Crop(rect),
info,
appendPixelTypeToFileName: false,
comparer: ImageComparer.Exact);
comparer: ImageComparer.Exact,
appendPixelTypeToFileName: false);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void CropUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 50, 50]);
image.Mutate(ctx => ctx.Crop(Rectangle.FromLTRB(20, 20, 50, 50)));
// The new subject area is now relative to the cropped area.
// overhanging pixels are constrained to the dimensions of the image.
Assert.Equal(
[0, 0],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
Assert.Equal(
[0, 0, 30, 30],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

78
tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs

@ -1,8 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
@ -12,12 +14,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
public class FlipTests
{
public static readonly TheoryData<FlipMode> FlipValues =
new TheoryData<FlipMode>
{
FlipMode.None,
FlipMode.Vertical,
FlipMode.Horizontal,
};
new()
{
FlipMode.None,
FlipMode.Vertical,
FlipMode.Horizontal
};
[Theory]
[WithTestPatternImages(nameof(FlipValues), 20, 37, PixelTypes.Rgba32)]
@ -25,23 +27,77 @@ public class FlipTests
[WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)]
public void Flip<TPixel>(TestImageProvider<TPixel> provider, FlipMode flipMode)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(
=> provider.RunValidatingProcessorTest(
ctx => ctx.Flip(flipMode),
testOutputDetails: flipMode,
appendPixelTypeToFileName: false);
}
[Theory]
[WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)]
public void Flip_WorksOnWrappedMemoryImage<TPixel>(TestImageProvider<TPixel> provider, FlipMode flipMode)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTestOnWrappedMemoryImage(
=> provider.RunValidatingProcessorTestOnWrappedMemoryImage(
ctx => ctx.Flip(flipMode),
testOutputDetails: flipMode,
useReferenceOutputFrom: nameof(this.Flip),
appendPixelTypeToFileName: false);
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void FlipVerticalUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 50, 50]);
image.Mutate(ctx => ctx.Flip(FlipMode.Vertical));
// The subject location is a single coordinate, so a vertical flip simply reflects its Y position:
// newY = imageHeight - originalY - 1
// This mirrors the point vertically around the image's horizontal axis, preserving its X coordinate.
Assert.Equal(
[5, 84],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is now inverted because a vertical flip reflects the image across
// the horizontal axis passing through the image center.
// The Y-coordinate of the top edge is recalculated as:
// newY = imageHeight - originalY - height - 1
Assert.Equal(
[5, 34, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void FlipHorizontalUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 50, 50]);
image.Mutate(ctx => ctx.Flip(FlipMode.Horizontal));
// The subject location is a single coordinate, so a horizontal flip simply reflects its X position:
// newX = imageWidth - originalX - 1
// This mirrors the point horizontally around the image's vertical axis, preserving its Y coordinate.
Assert.Equal(
[94, 15],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is now inverted because a horizontal flip reflects the image across
// the vertical axis passing through the image center.
// The X-coordinate of the left edge is recalculated as:
// newX = imageWidth - originalX - width - 1
Assert.Equal(
[44, 15, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

127
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs → tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs

@ -3,14 +3,16 @@
using System.Numerics;
using System.Reflection;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit.Abstractions;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Transforms;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
[Trait("Category", "Processors")]
public class ProjectiveTransformTests
@ -20,7 +22,7 @@ public class ProjectiveTransformTests
private ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
public static readonly TheoryData<string> ResamplerNames = new()
{
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box),
@ -39,7 +41,7 @@ public class ProjectiveTransformTests
nameof(KnownResamplers.Welch),
};
public static readonly TheoryData<TaperSide, TaperCorner> TaperMatrixData = new TheoryData<TaperSide, TaperCorner>
public static readonly TheoryData<TaperSide, TaperCorner> TaperMatrixData = new()
{
{ TaperSide.Bottom, TaperCorner.Both },
{ TaperSide.Bottom, TaperCorner.LeftOrTop },
@ -71,16 +73,14 @@ public class ProjectiveTransformTests
where TPixel : unmanaged, IPixel<TPixel>
{
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(TaperSide.Right, TaperCorner.Both, .5F);
using Image<TPixel> image = provider.GetImage();
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(TaperSide.Right, TaperCorner.Both, .5F);
image.Mutate(i => i.Transform(builder, sampler));
image.Mutate(i => i.Transform(builder, sampler));
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);
}
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);
}
[Theory]
@ -88,17 +88,15 @@ public class ProjectiveTransformTests
public void Transform_WithTaperMatrix<TPixel>(TestImageProvider<TPixel> provider, TaperSide taperSide, TaperCorner taperCorner)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(taperSide, taperCorner, .5F);
using Image<TPixel> image = provider.GetImage();
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(taperSide, taperCorner, .5F);
image.Mutate(i => i.Transform(builder));
image.Mutate(i => i.Transform(builder));
FormattableString testOutputDetails = $"{taperSide}-{taperCorner}";
image.DebugSave(provider, testOutputDetails);
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
}
FormattableString testOutputDetails = $"{taperSide}-{taperCorner}";
image.DebugSave(provider, testOutputDetails);
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
}
[Theory]
@ -106,17 +104,15 @@ public class ProjectiveTransformTests
public void Transform_WithQuadDistortion<TPixel>(TestImageProvider<TPixel> provider, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft);
using Image<TPixel> image = provider.GetImage();
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft);
image.Mutate(i => i.Transform(builder));
image.Mutate(i => i.Transform(builder));
FormattableString testOutputDetails = $"{topLeft}-{topRight}-{bottomRight}-{bottomLeft}";
image.DebugSave(provider, testOutputDetails);
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
}
FormattableString testOutputDetails = $"{topLeft}-{topRight}-{bottomRight}-{bottomLeft}";
image.DebugSave(provider, testOutputDetails);
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
}
[Theory]
@ -129,19 +125,17 @@ public class ProjectiveTransformTests
// This test matches the output described in the example at
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 matrix = Matrix4x4.Identity;
matrix.M14 = 0.01F;
using Image<TPixel> image = provider.GetImage();
Matrix4x4 matrix = Matrix4x4.Identity;
matrix.M14 = 0.01F;
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendMatrix(matrix);
image.Mutate(i => i.Transform(builder));
image.Mutate(i => i.Transform(builder));
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);
}
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);
}
[Theory]
@ -151,30 +145,28 @@ public class ProjectiveTransformTests
{
// https://jsfiddle.net/dFrHS/545/
// https://github.com/SixLabors/ImageSharp/issues/787
using (Image<TPixel> image = provider.GetImage())
{
using Image<TPixel> image = provider.GetImage();
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
Matrix4x4 matrix = new(
0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0,
52, 165, 0, 1);
Matrix4x4 matrix = new(
0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0,
52, 165, 0, 1);
#pragma warning restore SA1117 // Parameters should be on same line or separate lines
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendMatrix(matrix);
image.Mutate(i => i.Transform(builder));
image.Mutate(i => i.Transform(builder));
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);
}
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);
}
[Fact]
public void Issue1911()
{
using var image = new Image<Rgba32>(100, 100);
using Image<Rgba32> image = new(100, 100);
image.Mutate(x => x = x.Transform(new Rectangle(0, 0, 99, 100), Matrix4x4.Identity, new Size(99, 100), KnownResamplers.Lanczos2));
Assert.Equal(99, image.Width);
@ -240,13 +232,44 @@ public class ProjectiveTransformTests
ImageComparer.Exact.VerifySimilarity(img, img3);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void TransformUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 50, 50]);
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendRotationDegrees(180);
image.Mutate(ctx => ctx.Transform(builder));
// A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84)
Assert.Equal(
[94, 84],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50)
Assert.Equal(
[44, 34, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property is null)
{
throw new Exception($"No resampler named {name}");
throw new InvalidOperationException($"No resampler named {name}");
}
return (IResampler)property.GetValue(null);

76
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -3,10 +3,12 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
// ReSharper disable InconsistentNaming
@ -20,15 +22,15 @@ public class ResizeTests
public static readonly string[] AllResamplerNames = TestUtils.GetAllResamplerNames();
public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial };
public static readonly string[] CommonTestImages = [TestImages.Png.CalliphoraPartial];
public static readonly string[] SmokeTestResamplerNames =
{
[
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box),
nameof(KnownResamplers.Lanczos5),
};
];
private static readonly ImageComparer ValidatorComparer =
ImageComparer.TolerantPercentage(0.07F);
@ -78,7 +80,7 @@ public class ResizeTests
// resizing: (15, 12) -> (10, 6)
// kernel dimensions: (3, 4)
using Image<TPixel> image = provider.GetImage();
Size destSize = new Size(image.Width * wN / wD, image.Height * hN / hD);
Size destSize = new(image.Width * wN / wD, image.Height * hN / hD);
image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false));
FormattableString outputInfo = $"({wN}÷{wD},{hN}÷{hD})";
image.DebugSave(provider, outputInfo, appendPixelTypeToFileName: false);
@ -106,7 +108,7 @@ public class ResizeTests
Configuration configuration = Configuration.CreateDefaultInstance();
int workingBufferSizeHintInBytes = workingBufferLimitInRows * destSize.Width * SizeOfVector4;
TestMemoryAllocator allocator = new TestMemoryAllocator();
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
configuration.MemoryAllocator = allocator;
configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInBytes;
@ -211,7 +213,7 @@ public class ResizeTests
provider.RunValidatingProcessorTest(
x =>
{
ResizeOptions resizeOptions = new ResizeOptions()
ResizeOptions resizeOptions = new()
{
Size = x.GetCurrentSize() / 2,
Mode = ResizeMode.Crop,
@ -242,9 +244,7 @@ public class ResizeTests
[WithTestPatternImages(50, 50, CommonNonDefaultPixelTypes)]
public void Resize_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(x => x.Resize(x.GetCurrentSize() / 2), comparer: ValidatorComparer);
}
=> provider.RunValidatingProcessorTest(x => x.Resize(x.GetCurrentSize() / 2), comparer: ValidatorComparer);
[Theory]
[WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)]
@ -257,7 +257,7 @@ public class ResizeTests
using Image<TPixel> image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height);
Assert.ThrowsAny<Exception>(
() => { image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true)); });
() => image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true)));
}
[Theory]
@ -365,12 +365,12 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
Rectangle sourceRectangle = new Rectangle(
Rectangle sourceRectangle = new(
image.Width / 8,
image.Height / 8,
image.Width / 4,
image.Height / 4);
Rectangle destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
Rectangle destRectangle = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.Mutate(
x => x.Resize(
@ -437,7 +437,7 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions
ResizeOptions options = new()
{
Size = new Size(image.Width + 200, image.Height + 200),
Mode = ResizeMode.BoxPad,
@ -456,7 +456,8 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions { Size = new Size(image.Width, image.Height / 2) };
ResizeOptions options = new()
{ Size = new Size(image.Width, image.Height / 2) };
image.Mutate(x => x.Resize(options));
@ -470,7 +471,8 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions { Size = new Size(image.Width / 2, image.Height) };
ResizeOptions options = new()
{ Size = new Size(image.Width / 2, image.Height) };
image.Mutate(x => x.Resize(options));
@ -484,7 +486,7 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions
ResizeOptions options = new()
{
Size = new Size(480, 600),
Mode = ResizeMode.Crop
@ -502,7 +504,8 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions { Size = new Size(300, 300), Mode = ResizeMode.Max };
ResizeOptions options = new()
{ Size = new Size(300, 300), Mode = ResizeMode.Max };
image.Mutate(x => x.Resize(options));
@ -516,7 +519,7 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions
ResizeOptions options = new()
{
Size = new Size((int)Math.Round(image.Width * .75F), (int)Math.Round(image.Height * .95F)),
Mode = ResizeMode.Min
@ -534,7 +537,7 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions
ResizeOptions options = new()
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Pad,
@ -553,7 +556,7 @@ public class ResizeTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ResizeOptions options = new ResizeOptions
ResizeOptions options = new()
{
Size = new Size(image.Width / 2, image.Height),
Mode = ResizeMode.Stretch
@ -579,6 +582,7 @@ public class ResizeTests
}
using Image<TPixel> image = provider.GetImage();
// Don't bother saving, we're testing the EXIF metadata updates.
image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2));
}
@ -586,8 +590,8 @@ public class ResizeTests
[Fact]
public void Issue1195()
{
using Image<Rgba32> image = new Image<Rgba32>(2, 300);
Size size = new Size(50, 50);
using Image<Rgba32> image = new(2, 300);
Size size = new(50, 50);
image.Mutate(x => x
.Resize(
new ResizeOptions
@ -605,8 +609,8 @@ public class ResizeTests
[InlineData(3, 7)]
public void Issue1342(int width, int height)
{
using Image<Rgba32> image = new Image<Rgba32>(1, 1);
Size size = new Size(width, height);
using Image<Rgba32> image = new(1, 1);
Size size = new(width, height);
image.Mutate(x => x
.Resize(
new ResizeOptions
@ -630,4 +634,28 @@ public class ResizeTests
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void ResizeUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 20, 20]);
image.Mutate(ctx => ctx.Resize(new Size(image.Width / 2, image.Height / 2)));
// The transform operates in pixel space, so the resulting values correspond to the
// scaled pixel centers, producing whole-number coordinates after resizing.
Assert.Equal(
[2, 7],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
Assert.Equal(
[2, 7, 11, 11],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

56
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -1,8 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
@ -11,35 +13,59 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
public class RotateTests
{
public static readonly TheoryData<float> RotateAngles
= new TheoryData<float>
{
50, -50, 170, -170
};
= new()
{
50, -50, 170, -170
};
public static readonly TheoryData<RotateMode> RotateEnumValues
= new TheoryData<RotateMode>
{
RotateMode.None,
RotateMode.Rotate90,
RotateMode.Rotate180,
RotateMode.Rotate270
};
= new()
{
RotateMode.None,
RotateMode.Rotate90,
RotateMode.Rotate180,
RotateMode.Rotate270
};
[Theory]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
}
=> provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
[Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value)
where TPixel : unmanaged, IPixel<TPixel>
=> provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void RotateUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 50, 50]);
image.Mutate(ctx => ctx.Rotate(180));
// A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84)
Assert.Equal(
[94, 84],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50)
Assert.Equal(
[44, 34, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

40
tests/ImageSharp.Tests/Processing/Processors/Transforms/SwizzleTests.cs

@ -1,9 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
@ -12,17 +14,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms;
[GroupOutput("Transforms")]
public class SwizzleTests
{
private struct InvertXAndYSwizzler : ISwizzler
private readonly struct SwapXAndYSwizzler : ISwizzler
{
public InvertXAndYSwizzler(Size sourceSize)
{
this.DestinationSize = new Size(sourceSize.Height, sourceSize.Width);
}
public SwapXAndYSwizzler(Size sourceSize)
=> this.DestinationSize = new Size(sourceSize.Height, sourceSize.Width);
public Size DestinationSize { get; }
public Point Transform(Point point)
=> new Point(point.Y, point.X);
=> new(point.Y, point.X);
}
[Theory]
@ -35,15 +35,15 @@ public class SwizzleTests
using Image<TPixel> expectedImage = provider.GetImage();
using Image<TPixel> image = provider.GetImage();
image.Mutate(ctx => ctx.Swizzle(new InvertXAndYSwizzler(new Size(image.Width, image.Height))));
image.Mutate(ctx => ctx.Swizzle(new SwapXAndYSwizzler(new Size(image.Width, image.Height))));
image.DebugSave(
provider,
nameof(InvertXAndYSwizzler),
nameof(SwapXAndYSwizzler),
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: true);
image.Mutate(ctx => ctx.Swizzle(new InvertXAndYSwizzler(new Size(image.Width, image.Height))));
image.Mutate(ctx => ctx.Swizzle(new SwapXAndYSwizzler(new Size(image.Width, image.Height))));
image.DebugSave(
provider,
@ -53,4 +53,26 @@ public class SwizzleTests
ImageComparer.Exact.VerifySimilarity(expectedImage, image);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void SwizzleUpdatesSubject<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Metadata.ExifProfile = new();
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectLocation, [5, 15]);
image.Metadata.ExifProfile.SetValue(ExifTag.SubjectArea, [5, 15, 20, 20]);
image.Mutate(ctx => ctx.Swizzle(new SwapXAndYSwizzler(new Size(image.Width, image.Height))));
Assert.Equal(
[15, 5],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
Assert.Equal(
[15, 5, 20, 20],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

Loading…
Cancel
Save