From 668e223fa11bfbbf6f0c06b77df3351d1c06acfd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 12 Jun 2025 21:41:20 +1000 Subject: [PATCH] Update Subject EXIF metadata when transforming images. --- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 3 +- .../Formats/Cur/CurFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Cur/CurMetadata.cs | 3 +- .../Formats/Gif/GifFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Gif/GifMetadata.cs | 3 +- .../Formats/IFormatFrameMetadata.cs | 4 +- src/ImageSharp/Formats/IFormatMetadata.cs | 4 +- .../Formats/Ico/IcoFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Ico/IcoMetadata.cs | 3 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 3 +- src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 3 +- .../Formats/Png/PngFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 3 +- src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 3 +- src/ImageSharp/Formats/Tga/TgaMetadata.cs | 3 +- .../Formats/Tiff/TiffFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 3 +- .../Formats/Webp/WebpFrameMetadata.cs | 3 +- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 3 +- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 10 +- src/ImageSharp/Metadata/ImageMetadata.cs | 7 +- .../Metadata/Profiles/Exif/ExifProfile.cs | 69 ++++++++-- .../CloningImageProcessor{TPixel}.cs | 5 +- .../Processors/ImageProcessor{TPixel}.cs | 6 +- .../Transforms/CropProcessor{TPixel}.cs | 18 ++- .../EntropyCropProcessor{TPixel}.cs | 3 +- .../Processors/Transforms/ISwizzler.cs | 4 +- .../AffineTransformProcessor{TPixel}.cs | 5 + .../Linear/FlipProcessor{TPixel}.cs | 42 +++++- .../ProjectiveTransformProcessor{TPixel}.cs | 3 + .../Resize/ResizeProcessor{TPixel}.cs | 34 +++-- .../SwizzleProcessor{TSwizzler,TPixel}.cs | 16 ++- .../Transforms/SwizzleProcessor{TSwizzler}.cs | 5 +- .../Transforms/TransformProcessor{TPixel}.cs | 18 ++- .../Processors/Transforms/TransformUtils.cs | 24 +++- .../Processors/Transforms/CropTest.cs | 32 ++++- .../Processors/Transforms/FlipTests.cs | 78 +++++++++-- .../Transforms/ProjectiveTransformTests.cs | 127 +++++++++++------- .../Processors/Transforms/ResizeTests.cs | 76 +++++++---- .../Processors/Transforms/RotateTests.cs | 56 +++++--- .../Processors/Transforms/SwizzleTests.cs | 40 ++++-- 41 files changed, 560 insertions(+), 177 deletions(-) rename tests/ImageSharp.Tests/Processing/{ => Processors}/Transforms/ProjectiveTransformTests.cs (66%) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 1dac74ba3..3572687c9 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/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 public BmpMetadata DeepClone() => new(this); /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.ColorTable = null; } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 9854854aa..086025754 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/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 }; /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { float ratioX = destination.Width / (float)source.Width; diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index d8fdb3290..4c4d83dd8 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.ColorTable = null; diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index e1b3354ad..1a0deb8ba 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/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 } /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.LocalColorTable = null; diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 77f600633..978209b23 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.GlobalColorTable = null; diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs index 261cc1263..68959272c 100644 --- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs +++ b/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 /// The type of pixel format. /// The source image frame. /// The destination image frame. - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + /// The transformation matrix applied to the image frame. + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs index 3142b465c..81b344935 100644 --- a/src/ImageSharp/Formats/IFormatMetadata.cs +++ b/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 /// /// The type of pixel format. /// The destination image . - public void AfterImageApply(Image destination) + /// The transformation matrix applied to the image. + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index 31f65133e..e8656775d 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/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 }; /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { float ratioX = destination.Width / (float)source.Width; diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index f8c2ff40f..ae768d311 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.ColorTable = null; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index fe4855dc7..c620079bf 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index d852f3c8e..bef5da605 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index b8086cd6d..e337a8796 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/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 } /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 59ca3b17a..011672db8 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => this.ColorTable = null; diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index e463d511d..46ed2f210 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 8d40f8646..07aec8c3e 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index 189fee8b0..d9dfafbcc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/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 }; /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { float ratioX = destination.Width / (float)source.Width; diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index e965fcb4f..69b03f36f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index 3f976a640..8a6b30ab4 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/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 }; /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index db57bd8f2..51721c53e 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/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 }; /// - public void AfterImageApply(Image destination) + public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index b24aa140f..554afd69a 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/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 /// The type of pixel format. /// The source image frame. /// The destination image frame. - internal void AfterFrameApply(ImageFrame source, ImageFrame destination) + /// The transformation matrix applied to the frame. + internal void AfterFrameApply( + ImageFrame source, + ImageFrame destination, + Matrix4x4 matrix) where TPixel : unmanaged, IPixel { // 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 meta in this.formatMetadata) { - meta.Value.AfterFrameApply(source, destination); + meta.Value.AfterFrameApply(source, destination, matrix); } } } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 1961dbf19..918cf162a 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/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 /// /// The type of pixel format. /// The destination image. - internal void AfterImageApply(Image destination) + /// The transformation matrix applied to the image. + internal void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix); foreach (KeyValuePair meta in this.formatMetadata) { - meta.Value.AfterImageApply(destination); + meta.Value.AfterImageApply(destination, matrix); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index e91a69444..284c040b6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/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 { this.Parts = ExifParts.All; this.data = data; - this.InvalidTags = Array.Empty(); + this.InvalidTags = []; } /// @@ -161,11 +162,9 @@ public sealed class ExifProfile : IDeepCloneable return false; } - using (MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength)) - { - image = Image.Load(memStream); - return true; - } + using MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength); + image = Image.Load(memStream); + return true; } /// @@ -267,7 +266,7 @@ public sealed class ExifProfile : IDeepCloneable /// /// The tag of the exif value. /// The value. - /// Newly created value is null. + /// The newly created value is null. internal void SetValueInternal(ExifTag tag, object? value) { foreach (IExifValue exifValue in this.Values) @@ -312,6 +311,60 @@ public sealed class ExifProfile : IDeepCloneable } } + internal void SyncSubject(int width, int height, Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return; + } + + if (this.TryGetValue(ExifTag.SubjectLocation, out IExifValue? 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? 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); + } + } + } + /// /// Synchronizes the profiles with the specified metadata. /// diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs index bc34f759a..b641dceb5 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs +++ b/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 : ICloningImageProcessorThe source image. Cannot be null. /// The cloned/destination image. Cannot be null. protected virtual void AfterFrameApply(ImageFrame source, ImageFrame destination) - => destination.Metadata.AfterFrameApply(source, destination); + => destination.Metadata.AfterFrameApply(source, destination, Matrix4x4.Identity); /// /// This method is called after the process is applied to prepare the processor. /// /// The cloned/destination image. Cannot be null. protected virtual void AfterImageApply(Image destination) - => destination.Metadata.AfterImageApply(destination); + => destination.Metadata.AfterImageApply(destination, Matrix4x4.Identity); /// /// Disposes the object and frees resources for the Garbage Collector. diff --git a/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs index e1f7d1fff..3510fe7a8 100644 --- a/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs +++ b/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 : IImageProcessor /// /// The source image. Cannot be null. protected virtual void AfterFrameApply(ImageFrame source) - { - } + => source.Metadata.AfterFrameApply(source, source, Matrix4x4.Identity); /// /// This method is called after the process is applied to the complete image. /// protected virtual void AfterImageApply() - => this.Source.Metadata.AfterImageApply(this.Source); + => this.Source.Metadata.AfterImageApply(this.Source, Matrix4x4.Identity); /// /// Disposes the object and frees resources for the Garbage Collector. diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs index 1d82dd12a..e3341f9df 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs +++ b/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 : TransformProcessor where TPixel : unmanaged, IPixel { private readonly Rectangle cropRectangle; + private readonly Matrix4x4 transformMatrix; /// /// Initializes a new instance of the class. @@ -26,10 +28,20 @@ internal class CropProcessor : TransformProcessor /// The source area to process for the current processor instance. public CropProcessor(Configuration configuration, CropProcessor definition, Image 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); + } /// - 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; /// protected override void OnFrameApply(ImageFrame source, ImageFrame destination) @@ -50,7 +62,7 @@ internal class CropProcessor : TransformProcessor 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, diff --git a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs index 2eda61e41..8a76ca42f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs +++ b/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 : ImageProcessor // 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 temp = new(this.Configuration, this.Source.Metadata.DeepClone(), new[] { this.Source.Frames.RootFrame.Clone() })) + using (Image temp = new(this.Configuration, this.Source.Metadata.DeepClone(), [this.Source.Frames.RootFrame.Clone()])) { Configuration configuration = this.Source.Configuration; diff --git a/src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs b/src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs index 321ecf968..3c11c32b4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ISwizzler.cs @@ -11,12 +11,12 @@ public interface ISwizzler /// /// Gets the size of the image after transformation. /// - Size DestinationSize { get; } + public Size DestinationSize { get; } /// /// Applies the swizzle transformation to a given point. /// /// Point to transform. /// The transformed point. - Point Transform(Point point); + public Point Transform(Point point); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index b3919e584..59f5773cf 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -18,6 +18,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR { private readonly Size destinationSize; private readonly Matrix3x2 transformMatrix; + private readonly Matrix4x4 transformMatrix4x4; private readonly IResampler resampler; private ImageFrame? source; private ImageFrame? destination; @@ -34,6 +35,7 @@ internal class AffineTransformProcessor : TransformProcessor, 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 : TransformProcessor, IR this.resampler.ApplyTransform(this); } + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix4x4; + /// public void ApplyTransform(in TResampler sampler) where TResampler : struct, IResampler diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs index 1adda1d04..86ba2f0f9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs +++ b/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 : ImageProcessor where TPixel : unmanaged, IPixel { private readonly FlipProcessor definition; + private readonly Matrix4x4 transformMatrix; /// /// Initializes a new instance of the class. /// - /// The configuration which allows altering default behaviour or extending the library. + /// The configuration which allows altering default behavior or extending the library. /// The . /// The source for the current processor instance. /// The source area to process for the current processor instance. public FlipProcessor(Configuration configuration, FlipProcessor definition, Image 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); + } /// protected override void OnFrameApply(ImageFrame source) @@ -43,6 +73,14 @@ internal class FlipProcessor : ImageProcessor } } + /// + protected override void AfterFrameApply(ImageFrame source) + => source.Metadata.AfterFrameApply(source, source, this.transformMatrix); + + /// + protected override void AfterImageApply() + => this.Source.Metadata.AfterImageApply(this.Source, this.transformMatrix); + /// /// Swaps the image at the X-axis, which goes horizontally through the middle at half the height of the image. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index 16b0070e9..1c30fd114 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -47,6 +47,9 @@ internal class ProjectiveTransformProcessor : TransformProcessor this.resampler.ApplyTransform(this); } + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + /// public void ApplyTransform(in TResampler sampler) where TResampler : struct, IResampler diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index cfc30edc0..49439545b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/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 : TransformProcessor, IResampling private readonly IResampler resampler; private readonly Rectangle destinationRectangle; private Image? destination; + private readonly Matrix4x4 transformMatrix; public ResizeProcessor(Configuration configuration, ResizeProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) @@ -30,6 +32,17 @@ internal class ResizeProcessor : TransformProcessor, 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); } /// @@ -50,6 +63,9 @@ internal class ResizeProcessor : TransformProcessor, IResampling // Everything happens in BeforeImageApply. } + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + public void ApplyTransform(in TResampler sampler) where TResampler : struct, IResampler { @@ -81,7 +97,7 @@ internal class ResizeProcessor : TransformProcessor, 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 : TransformProcessor, 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 : TransformProcessor, 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 : TransformProcessor, 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 : TransformProcessor, IResampling // To reintroduce parallel processing, we would launch multiple workers // for different row intervals of the image. - using var worker = new ResizeWorker( + using ResizeWorker worker = new( configuration, sourceRegion, conversionModifiers, @@ -218,7 +232,7 @@ internal class ResizeProcessor : TransformProcessor, 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); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs index 1ea2156c8..3bec82cce 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs +++ b/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 : TransformProcessor { private readonly TSwizzler swizzler; private readonly Size destinationSize; + private readonly Matrix4x4 transformMatrix; public SwizzleProcessor(Configuration configuration, TSwizzler swizzler, Image 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; + /// + protected override Size GetDestinationSize() => this.destinationSize; + + /// + protected override Matrix4x4 GetTransformMatrix() => this.transformMatrix; + /// protected override void OnFrameApply(ImageFrame source, ImageFrame destination) { Point p = default; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs b/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs index 0b8274aff..d00e01711 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs @@ -16,10 +16,7 @@ public sealed class SwizzleProcessor : IImageProcessor /// Initializes a new instance of the class. /// /// The swizzler operation. - public SwizzleProcessor(TSwizzler swizzler) - { - this.Swizzler = swizzler; - } + public SwizzleProcessor(TSwizzler swizzler) => this.Swizzler = swizzler; /// /// Gets the swizzler operation. diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs index a8455a06e..0d40cd32f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs +++ b/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; /// @@ -23,4 +23,20 @@ internal abstract class TransformProcessor : CloningImageProcessor + /// Gets the transform matrix that will be applied to the image. + /// + /// + /// The that represents the transformation to be applied to the image. + /// + protected abstract Matrix4x4 GetTransformMatrix(); + + /// + protected override void AfterFrameApply(ImageFrame source, ImageFrame destination) + => destination.Metadata.AfterFrameApply(source, destination, this.GetTransformMatrix()); + + /// + protected override void AfterImageApply(Image destination) + => destination.Metadata.AfterImageApply(destination, this.GetTransformMatrix()); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 47b3250b8..b7afea449 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -419,6 +419,28 @@ internal static class TransformUtils return size; } + /// + /// Attempts to derive a 4x4 projective transform matrix that approximates the behavior of an . + /// + /// + /// The swizzler to use for the transformation. + /// + /// + /// The source rectangle that defines the area to be transformed. + /// + /// + /// The type of the swizzler, which must implement . + /// + public static Matrix4x4 GetSwizzlerMatrix(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); + /// /// Returns the size relative to the source for the given transformation matrix. /// @@ -504,7 +526,7 @@ internal static class TransformUtils /// if the transformation was successful; otherwise, . /// [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)) { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs index 18a7a9bd0..56e9a5201 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs +++ b/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(TestImageProvider provider, int x, int y, int w, int h) where TPixel : unmanaged, IPixel { - 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs index 717582274..9aa04e370 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs +++ b/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 FlipValues = - new TheoryData - { - 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(TestImageProvider provider, FlipMode flipMode) where TPixel : unmanaged, IPixel - { - 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(TestImageProvider provider, FlipMode flipMode) where TPixel : unmanaged, IPixel - { - 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs similarity index 66% rename from tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs rename to tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs index 6b6db69c1..2e580ea9f 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/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 ResamplerNames = new TheoryData + public static readonly TheoryData ResamplerNames = new() { nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Box), @@ -39,7 +41,7 @@ public class ProjectiveTransformTests nameof(KnownResamplers.Welch), }; - public static readonly TheoryData TaperMatrixData = new TheoryData + public static readonly TheoryData TaperMatrixData = new() { { TaperSide.Bottom, TaperCorner.Both }, { TaperSide.Bottom, TaperCorner.LeftOrTop }, @@ -71,16 +73,14 @@ public class ProjectiveTransformTests where TPixel : unmanaged, IPixel { IResampler sampler = GetResampler(resamplerName); - using (Image image = provider.GetImage()) - { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() - .AppendTaper(TaperSide.Right, TaperCorner.Both, .5F); + using Image 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(TestImageProvider provider, TaperSide taperSide, TaperCorner taperCorner) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() - .AppendTaper(taperSide, taperCorner, .5F); + using Image 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(TestImageProvider provider, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() - .AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft); + using Image 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 image = provider.GetImage()) - { - Matrix4x4 matrix = Matrix4x4.Identity; - matrix.M14 = 0.01F; + using Image 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 image = provider.GetImage()) - { + using Image 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(100, 100); + using Image 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index d1b005ee4..f9cf10d22 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/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 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(TestImageProvider provider) where TPixel : unmanaged, IPixel - { - 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 image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height); Assert.ThrowsAny( - () => { 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 { using Image 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 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 image = new Image(2, 300); - Size size = new Size(50, 50); + using Image 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 image = new Image(1, 1); - Size size = new Size(width, height); + using Image 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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); + } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index b150da1d8..dfa263fec 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/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 RotateAngles - = new TheoryData - { - 50, -50, 170, -170 - }; + = new() + { + 50, -50, 170, -170 + }; public static readonly TheoryData RotateEnumValues - = new TheoryData - { - 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(TestImageProvider provider, float value) where TPixel : unmanaged, IPixel - { - 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(TestImageProvider provider, RotateMode value) where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false); + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void RotateUpdatesSubject(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false); + using Image 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); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SwizzleTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SwizzleTests.cs index de02502e2..04f11644b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SwizzleTests.cs +++ b/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 expectedImage = provider.GetImage(); using Image 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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); + } }