From 5dd3dda055384068740f3d44e20ae0308e247c4b Mon Sep 17 00:00:00 2001
From: James Jackson-South <james_south@hotmail.com>
Date: Tue, 23 Jul 2024 21:18:39 +1000
Subject: [PATCH] Update refs and fix EXIF handling.

---
 .../ImageSharp.Web.Sample/Pages/IndexModel.cs | 17 ++--
 .../ExifOrientationUtilities.cs               | 80 ++++++-------------
 src/ImageSharp.Web/ImageSharp.Web.csproj      |  4 +-
 .../Processors/ResizeWebProcessor.cs          | 27 ++++++-
 .../TagHelpers/HmacTokenTagHelper.cs          |  2 +-
 .../TagHelpers/ImageTagHelper.cs              |  2 +-
 .../Helpers/ExifOrientationUtilitiesTests.cs  | 18 ++---
 7 files changed, 71 insertions(+), 79 deletions(-)

diff --git a/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs b/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs
index fd22ab50..eb1f835e 100644
--- a/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs
+++ b/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs
@@ -1,14 +1,13 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
 
 using Microsoft.AspNetCore.Mvc.RazorPages;
 
-namespace ImageSharp.Web.Sample.Pages
+namespace ImageSharp.Web.Sample.Pages;
+
+/// <summary>
+/// Defines the index page view model.
+/// </summary>
+public class IndexModel : PageModel
 {
-    /// <summary>
-    /// Defines the index page view model.
-    /// </summary>
-    public class IndexModel : PageModel
-    {
-    }
 }
diff --git a/src/ImageSharp.Web/ExifOrientationUtilities.cs b/src/ImageSharp.Web/ExifOrientationUtilities.cs
index 2995c378..bd588e25 100644
--- a/src/ImageSharp.Web/ExifOrientationUtilities.cs
+++ b/src/ImageSharp.Web/ExifOrientationUtilities.cs
@@ -25,50 +25,34 @@ public static class ExifOrientationUtilities
     /// </returns>
     public static Vector2 Transform(Vector2 position, Vector2 min, Vector2 max, ushort orientation)
     {
-        if (orientation is <= ExifOrientationMode.TopLeft or > ExifOrientationMode.LeftBottom)
-        {
-            // Short circuit orientations that are not transformed below
-            return position;
-        }
-
         // New XY is calculated based on flipping and rotating the input XY.
-        // Coordinate ranges are normalized to a range of 0-1 so we can pass a
-        // constant integer size to the transform builder.
-        Vector2 scaled = Scale(position, min, max);
-        AffineTransformBuilder builder = new();
-        Size size = new(1, 1);
-        switch (orientation)
+        Vector2 bounds = max - min;
+        return orientation switch
         {
-            case ExifOrientationMode.TopRight:
-                builder.AppendTranslation(new Vector2(FlipScaled(scaled.X), 0));
-                break;
-            case ExifOrientationMode.BottomRight:
-                builder.AppendRotationDegrees(180);
-                break;
-            case ExifOrientationMode.BottomLeft:
-                builder.AppendTranslation(new Vector2(0, FlipScaled(scaled.Y)));
-                break;
-            case ExifOrientationMode.LeftTop:
-                builder.AppendTranslation(new Vector2(FlipScaled(scaled.X), 0));
-                builder.AppendRotationDegrees(270);
-                break;
-            case ExifOrientationMode.RightTop:
-                builder.AppendRotationDegrees(270);
-                break;
-            case ExifOrientationMode.RightBottom:
-                builder.AppendTranslation(new Vector2(FlipScaled(scaled.X), 0));
-                builder.AppendRotationDegrees(90);
-                break;
-            case ExifOrientationMode.LeftBottom:
-                builder.AppendRotationDegrees(90);
-                break;
-            default:
-                // Use identity matrix.
-                break;
-        }
+            // 0 degrees, mirrored: image has been flipped back-to-front.
+            ExifOrientationMode.TopRight => new Vector2(Flip(position.X, bounds.X), position.Y),
+
+            // 180 degrees: image is upside down.
+            ExifOrientationMode.BottomRight => new Vector2(Flip(position.X, bounds.X), Flip(position.Y, bounds.Y)),
+
+            // 180 degrees, mirrored: image has been flipped back-to-front and is upside down.
+            ExifOrientationMode.BottomLeft => new Vector2(position.X, Flip(position.Y, bounds.Y)),
+
+            // 90 degrees: image has been flipped back-to-front and is on its side.
+            ExifOrientationMode.LeftTop => new Vector2(position.Y, position.X),
+
+            // 90 degrees, mirrored: image is on its side.
+            ExifOrientationMode.RightTop => new Vector2(position.Y, Flip(position.X, bounds.X)),
+
+            // 270 degrees: image has been flipped back-to-front and is on its far side.
+            ExifOrientationMode.RightBottom => new Vector2(Flip(position.Y, bounds.Y), Flip(position.X, bounds.X)),
+
+            // 270 degrees, mirrored: image is on its far side.
+            ExifOrientationMode.LeftBottom => new Vector2(Flip(position.Y, bounds.Y), position.X),
 
-        Matrix3x2 matrix = builder.BuildMatrix(size);
-        return DeScale(Vector2.Transform(scaled, matrix), SwapXY(min, orientation), SwapXY(max, orientation));
+            // 0 degrees: the correct orientation, no adjustment is required.
+            _ => position,
+        };
     }
 
     /// <summary>
@@ -223,17 +207,5 @@ or ExifOrientationMode.RightBottom
         };
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    private static Vector2 Scale(Vector2 x, Vector2 min, Vector2 max) => (x - min) / (max - min);
-
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    private static Vector2 DeScale(Vector2 x, Vector2 min, Vector2 max) => min + (x * (max - min));
-
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    private static float FlipScaled(float origin) => (2F * -origin) + 1F;
-
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    private static Vector2 SwapXY(Vector2 position, ushort orientation)
-        => IsExifOrientationRotated(orientation)
-        ? new Vector2(position.Y, position.X)
-        : position;
+    private static float Flip(float offset, float max) => max - offset;
 }
diff --git a/src/ImageSharp.Web/ImageSharp.Web.csproj b/src/ImageSharp.Web/ImageSharp.Web.csproj
index 7a1f1f3e..6330edb6 100644
--- a/src/ImageSharp.Web/ImageSharp.Web.csproj
+++ b/src/ImageSharp.Web/ImageSharp.Web.csproj
@@ -45,8 +45,8 @@
 
   <ItemGroup>
     <FrameworkReference Include="Microsoft.AspNetCore.App" />
-    <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
-    <PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
+    <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
+    <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
   </ItemGroup>
 
   <Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" />
diff --git a/src/ImageSharp.Web/Processors/ResizeWebProcessor.cs b/src/ImageSharp.Web/Processors/ResizeWebProcessor.cs
index 40898561..4004bfba 100644
--- a/src/ImageSharp.Web/Processors/ResizeWebProcessor.cs
+++ b/src/ImageSharp.Web/Processors/ResizeWebProcessor.cs
@@ -3,6 +3,7 @@
 
 using System.Globalization;
 using System.Numerics;
+using System.Runtime.CompilerServices;
 using Microsoft.Extensions.Logging;
 using SixLabors.ImageSharp.Metadata.Profiles.Exif;
 using SixLabors.ImageSharp.Processing;
@@ -131,7 +132,7 @@ public FormattedImage Process(
         return new()
         {
             Size = size,
-            CenterCoordinates = GetCenter(orientation, commands, parser, culture),
+            CenterCoordinates = GetCenter(image, orientation, commands, parser, culture),
             Position = GetAnchor(orientation, commands, parser, culture),
             Mode = mode,
             Compand = GetCompandMode(commands, parser, culture),
@@ -162,6 +163,7 @@ private static Size ParseSize(
     }
 
     private static PointF? GetCenter(
+        FormattedImage image,
         ushort orientation,
         CommandCollection commands,
         CommandParser parser,
@@ -179,8 +181,21 @@ private static Size ParseSize(
             return null;
         }
 
-        Vector2 center = new(coordinates[0], coordinates[1]);
-        return ExifOrientationUtilities.Transform(center, Vector2.Zero, Vector2.One, orientation);
+        // Coordinates for the center point are given as a percentage.
+        // We must convert these to pixel values for transformation then convert back.
+        //
+        // Get the display size of the image after orientation is applied.
+        Size size = ExifOrientationUtilities.Transform(new Size(image.Image.Width, image.Image.Height), orientation);
+        Vector2 min = Vector2.Zero;
+        Vector2 max = new(size.Width, size.Height);
+
+        // Scale pixel values up to image height and transform.
+        Vector2 center = DeScale(new Vector2(coordinates[0], coordinates[1]), min, max);
+        Vector2 transformed = ExifOrientationUtilities.Transform(center, min, max, orientation);
+
+        // Now scale pixel values down as percentage of real image height.
+        max = new Vector2(image.Image.Width, image.Image.Height);
+        return Scale(transformed, min, max);
     }
 
     private static ResizeMode GetMode(
@@ -252,4 +267,10 @@ private static ushort GetExifOrientation(FormattedImage image, CommandCollection
         image.TryGetExifOrientation(out ushort orientation);
         return orientation;
     }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static Vector2 Scale(Vector2 x, Vector2 min, Vector2 max) => (x - min) / (max - min);
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static Vector2 DeScale(Vector2 x, Vector2 min, Vector2 max) => min + (x * (max - min));
 }
diff --git a/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs b/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs
index 3aef7500..81e25622 100644
--- a/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs
+++ b/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs
@@ -29,7 +29,7 @@ public class HmacTokenTagHelper : UrlResolutionTagHelper
     /// <param name="options">The middleware configuration options.</param>
     /// <param name="authorizationUtilities">Contains helpers that allow authorization of image requests.</param>
     /// <param name="urlHelperFactory">The URL helper factory.</param>
-    /// <param name="htmlEncoder">The HTML encorder.</param>
+    /// <param name="htmlEncoder">The HTML encoder.</param>
     public HmacTokenTagHelper(
         IOptions<ImageSharpMiddlewareOptions> options,
         RequestAuthorizationUtilities authorizationUtilities,
diff --git a/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs b/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs
index 72b5c2cd..cf7c675e 100644
--- a/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs
+++ b/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs
@@ -154,7 +154,7 @@ public ImageTagHelper(
 
     /// <summary>
     /// Gets or sets a value indicating whether to automatically
-    /// rotate/flip the iput image based on embedded EXIF orientation property values
+    /// rotate/flip the input image based on embedded EXIF orientation property values
     /// before processing.
     /// </summary>
     [HtmlAttributeName(AutoOrientAttributeName)]
diff --git a/tests/ImageSharp.Web.Tests/Helpers/ExifOrientationUtilitiesTests.cs b/tests/ImageSharp.Web.Tests/Helpers/ExifOrientationUtilitiesTests.cs
index c553a743..497c3029 100644
--- a/tests/ImageSharp.Web.Tests/Helpers/ExifOrientationUtilitiesTests.cs
+++ b/tests/ImageSharp.Web.Tests/Helpers/ExifOrientationUtilitiesTests.cs
@@ -26,15 +26,15 @@ public class ExifOrientationUtilitiesTests
     public static TheoryData<Vector2, Vector2, Vector2, ushort, Vector2> TransformVectorData =
         new()
         {
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.Unknown, new Vector2(25F, 25F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.TopLeft, new Vector2(25F, 25F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.TopRight, new Vector2(125F, 25F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.BottomRight, new Vector2(125F, 75F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.BottomLeft, new Vector2(25F, 75F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.LeftTop, new Vector2(25F, 25F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.RightTop, new Vector2(25F, 125F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.RightBottom, new Vector2(75F, 125F) },
-            { new Vector2(25F, 25F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.LeftBottom, new Vector2(75F, 25F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.Unknown, new Vector2(24F, 26F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.TopLeft, new Vector2(24F, 26F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.TopRight, new Vector2(126F, 26F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.BottomRight, new Vector2(126F, 74F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.BottomLeft, new Vector2(24F, 74F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.LeftTop, new Vector2(26F, 24F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.RightTop, new Vector2(26F, 126F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.RightBottom, new Vector2(74F, 126F) },
+            { new Vector2(24F, 26F), Vector2.Zero, new Vector2(150, 100), ExifOrientationMode.LeftBottom, new Vector2(74F, 24F) },
         };
 
     [Theory]