From 5dd3dda055384068740f3d44e20ae0308e247c4b Mon Sep 17 00:00:00 2001 From: James Jackson-South 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; + +/// +/// Defines the index page view model. +/// +public class IndexModel : PageModel { - /// - /// Defines the index page view model. - /// - 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 /// 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, + }; } /// @@ -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 @@ - - + + 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 /// The middleware configuration options. /// Contains helpers that allow authorization of image requests. /// The URL helper factory. - /// The HTML encorder. + /// The HTML encoder. public HmacTokenTagHelper( IOptions 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( /// /// 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. /// [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 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]