From ff153cdc408114542ae56816813bbb93c5df41f4 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:08:54 +0200 Subject: [PATCH 01/20] Update to ImageSharp 2.1.0 and ImageSharp.Web 2.0.0-alpha.0.23 --- NuGet.config | 6 ++++++ src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 2 +- .../ImageProcessors/CropWebProcessor.cs | 9 ++++++--- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 +- .../ImageProcessors/CropWebProcessorTests.cs | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 NuGet.config diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000000..6aa8697b8fe9 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 8bc4956c2f3d..40cda9903844 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 30331fd812fd..82a517cc9cce 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build // options.Configuration is set using ImageSharpConfigurationOptions below options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CachedNameLength = imagingSettings.Cache.CachedNameLength; + options.CacheHashLength = imagingSettings.Cache.CachedNameLength; // Use configurable maximum width and height (overwrite ImageSharps default) options.OnParseCommandsAsync = context => diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 7b3cc817f232..5d6d8062b594 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -27,7 +27,7 @@ public class CropWebProcessor : IImageWebProcessor }; /// - public FormattedImage Process(FormattedImage image, ILogger logger, IDictionary commands, CommandParser parser, CultureInfo culture) + public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) { RectangleF? coordinates = GetCoordinates(commands, parser, culture); if (coordinates != null) @@ -41,14 +41,17 @@ public FormattedImage Process(FormattedImage image, ILogger logger, IDictionary< int height = (int)MathF.Round(coordinates.Value.Height * sourceHeight); var cropRectangle = new Rectangle(x, y, width, height); - + image.Image.Mutate(x => x.Crop(cropRectangle)); } return image; } - private static RectangleF? GetCoordinates(IDictionary commands, CommandParser parser, CultureInfo culture) + /// + public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; + + private static RectangleF? GetCoordinates(CommandCollection commands, CommandParser parser, CultureInfo culture) { float[] coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index b023b5ecdc81..545bb05308ae 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index 2c508d97d200..a5811c068165 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -31,7 +31,7 @@ public void CropWebProcessor_CropsImage() var parser = new CommandParser(converters); CultureInfo culture = CultureInfo.InvariantCulture; - var commands = new Dictionary + var commands = new CommandCollection { { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom }; From e8d82af8d29cd69ea62af14887fef288cbb444f0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:15:58 +0200 Subject: [PATCH 02/20] Rename CachedNameLength to CacheHashLength and add CacheFolderDepth setting --- .../Models/ImagingCacheSettings.cs | 19 +++++++++++++------ .../UmbracoBuilder.ImageSharp.cs | 10 ++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs index cd7d2fda1bc7..b3bdddc211d3 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs @@ -14,8 +14,9 @@ public class ImagingCacheSettings { internal const string StaticBrowserMaxAge = "7.00:00:00"; internal const string StaticCacheMaxAge = "365.00:00:00"; - internal const int StaticCachedNameLength = 8; - internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + internal const int StaticCacheHashLength = 12; + internal const int StaticCacheFolderDepth = 8; + internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; /// /// Gets or sets a value for the browser image cache maximum age. @@ -30,13 +31,19 @@ public class ImagingCacheSettings public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); /// - /// Gets or sets a value for length of the cached name. + /// Gets or sets a value for the image cache hash length. /// - [DefaultValue(StaticCachedNameLength)] - public uint CachedNameLength { get; set; } = StaticCachedNameLength; + [DefaultValue(StaticCacheHashLength)] + public uint CacheHashLength { get; set; } = StaticCacheHashLength; /// - /// Gets or sets a value for the cache folder. + /// Gets or sets a value for the image cache folder depth. + /// + [DefaultValue(StaticCacheFolderDepth)] + public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; + + /// + /// Gets or sets a value for the image cache folder. /// [DefaultValue(StaticCacheFolder)] public string CacheFolder { get; set; } = StaticCacheFolder; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 82a517cc9cce..7f47aad30ac8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build // options.Configuration is set using ImageSharpConfigurationOptions below options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CacheHashLength = imagingSettings.Cache.CachedNameLength; + options.CacheHashLength = imagingSettings.Cache.CacheHashLength; // Use configurable maximum width and height (overwrite ImageSharps default) options.OnParseCommandsAsync = context => @@ -71,7 +71,13 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; }; }) - .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) + // Configure cache options + .Configure(options => + { + options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder); + options.CacheFolderDepth = imagingSettings.Cache.CacheFolderDepth; + }) + // Add custom processors .AddProcessor(); // Configure middleware to use the registered/shared ImageSharp configuration From 5fa02e936dcd80f7998747a1818773ca71a82d07 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:17:21 +0200 Subject: [PATCH 03/20] Replace PhysicalFileSystemProvider with WebRootImageProvider --- .../UmbracoBuilder.ImageSharp.cs | 4 ++ .../ImageProviders/FileInfoImageResolver.cs | 34 ++++++++++ .../FileProviderImageProvider.cs | 62 +++++++++++++++++++ .../ImageProviders/WebRootImageProvider.cs | 20 ++++++ 4 files changed, 120 insertions(+) create mode 100644 src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs create mode 100644 src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs create mode 100644 src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 7f47aad30ac8..c05344d7d140 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -10,10 +10,12 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; +using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; +using Umbraco.Cms.Web.Common.ImageProviders; namespace Umbraco.Extensions { @@ -71,6 +73,8 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; }; }) + // Replace default image provider + .RemoveProvider().AddProvider() // Configure cache options .Configure(options => { diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs new file mode 100644 index 000000000000..3e6050b4c5e2 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Resolvers; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Provides means to manage image buffers from an instance. + /// + public class FileInfoImageResolver : IImageResolver + { + private readonly IFileInfo _fileInfo; + private readonly ImageMetadata _metadata; + + /// + /// Initializes a new instance of the class. + /// + /// The file info. + /// The image metadata associated with this file. + public FileInfoImageResolver(IFileInfo fileInfo, in ImageMetadata metadata) + { + _fileInfo = fileInfo; + _metadata = metadata; + } + + /// + public Task GetMetaDataAsync() => Task.FromResult(_metadata); + + /// + public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); + } +} diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs new file mode 100644 index 000000000000..1b5002b9ded4 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.FileProviders; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Providers; +using SixLabors.ImageSharp.Web.Resolvers; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Returns images from an abstraction. + /// + public class FileProviderImageProvider : IImageProvider + { + /// + /// The file provider abstraction. + /// + private readonly IFileProvider _fileProvider; + + /// + /// Contains various format helper methods based on the current configuration. + /// + private readonly FormatUtilities _formatUtilities; + + /// + /// Initializes a new instance of the class. + /// + /// The file provider. + /// Contains various format helper methods based on the current configuration. + public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities formatUtilities) + { + _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); + _formatUtilities = formatUtilities ?? throw new ArgumentNullException(nameof(formatUtilities)); + } + + /// + public ProcessingBehavior ProcessingBehavior { get; protected set; } = ProcessingBehavior.CommandOnly; + + /// + public Func Match { get; set; } = _ => true; + + /// + public virtual bool IsValidRequest(HttpContext context) + => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); + + /// + public virtual Task GetAsync(HttpContext context) + { + IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); + if (!fileInfo.Exists) + { + return Task.FromResult(null); + } + + var metadata = new ImageMetadata(fileInfo.LastModified.UtcDateTime, fileInfo.Length); + + return Task.FromResult(new FileInfoImageResolver(fileInfo, metadata)); + } + } +} diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs new file mode 100644 index 000000000000..a21762e81ef6 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using SixLabors.ImageSharp.Web; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Returns images from the web root file provider. + /// + public sealed class WebRootImageProvider : FileProviderImageProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The web host environment. + /// Contains various format helper methods based on the current configuration. + public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) + : base(environment?.WebRootFileProvider, formatUtilities) + { } + } +} From da34c0906164f0a2c1317e26aac2312df4ed3d32 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 10:56:02 +0200 Subject: [PATCH 04/20] Support EXIF-orientation in image dimention extractor --- .../Media/ImageSharpDimensionExtractor.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 227d9a653ce4..27f49b89558d 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs @@ -1,5 +1,7 @@ +using System; using System.IO; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; @@ -30,10 +32,40 @@ public ImageSharpDimensionExtractor(Configuration configuration) IImageInfo imageInfo = Image.Identify(_configuration, stream); if (imageInfo != null) { - size = new Size(imageInfo.Width, imageInfo.Height); + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); } return size; } + + private static bool IsExifOrientationRotated(IImageInfo imageInfo) + => GetExifOrientation(imageInfo) switch + { + ExifOrientationMode.LeftTop + or ExifOrientationMode.RightTop + or ExifOrientationMode.RightBottom + or ExifOrientationMode.LeftBottom => true, + _ => false, + }; + + private static ushort GetExifOrientation(IImageInfo imageInfo) + { + IExifValue orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); + if (orientation is not null) + { + if (orientation.DataType == ExifDataType.Short) + { + return orientation.Value; + } + else + { + return Convert.ToUInt16(orientation.Value); + } + } + + return ExifOrientationMode.Unknown; + } } } From 91fbdb27b88f3ab3085f9cd58a0d761666f09e88 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 31 Mar 2022 00:31:11 +0200 Subject: [PATCH 05/20] Remove virtual methods on FileProviderImageProvider --- .../ImageProviders/FileProviderImageProvider.cs | 4 ++-- src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs index 1b5002b9ded4..44a2795a00d9 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -42,11 +42,11 @@ public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities for public Func Match { get; set; } = _ => true; /// - public virtual bool IsValidRequest(HttpContext context) + public bool IsValidRequest(HttpContext context) => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); /// - public virtual Task GetAsync(HttpContext context) + public Task GetAsync(HttpContext context) { IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); if (!fileInfo.Exists) diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs index a21762e81ef6..73605a6b7cc1 100644 --- a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs @@ -11,10 +11,10 @@ public sealed class WebRootImageProvider : FileProviderImageProvider /// /// Initializes a new instance of the class. /// - /// The web host environment. + /// The web hosting environment. /// Contains various format helper methods based on the current configuration. public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) - : base(environment?.WebRootFileProvider, formatUtilities) + : base(environment.WebRootFileProvider, formatUtilities) { } } } From 827ae60ec6c4929e8560a3ae8b39c46c325320fb Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 31 Mar 2022 00:33:08 +0200 Subject: [PATCH 06/20] Simplify FileInfoImageResolver --- .../ImageProviders/FileInfoImageResolver.cs | 11 +++-------- .../ImageProviders/FileProviderImageProvider.cs | 4 +--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs index 3e6050b4c5e2..4275cd137f09 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs @@ -12,21 +12,16 @@ namespace Umbraco.Cms.Web.Common.ImageProviders public class FileInfoImageResolver : IImageResolver { private readonly IFileInfo _fileInfo; - private readonly ImageMetadata _metadata; /// /// Initializes a new instance of the class. /// /// The file info. - /// The image metadata associated with this file. - public FileInfoImageResolver(IFileInfo fileInfo, in ImageMetadata metadata) - { - _fileInfo = fileInfo; - _metadata = metadata; - } + public FileInfoImageResolver(IFileInfo fileInfo) + => _fileInfo = fileInfo; /// - public Task GetMetaDataAsync() => Task.FromResult(_metadata); + public Task GetMetaDataAsync() => Task.FromResult(new ImageMetadata(_fileInfo.LastModified.UtcDateTime, _fileInfo.Length)); /// public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs index 44a2795a00d9..f41c6ba8502b 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -54,9 +54,7 @@ public Task GetAsync(HttpContext context) return Task.FromResult(null); } - var metadata = new ImageMetadata(fileInfo.LastModified.UtcDateTime, fileInfo.Length); - - return Task.FromResult(new FileInfoImageResolver(fileInfo, metadata)); + return Task.FromResult(new FileInfoImageResolver(fileInfo)); } } } From 8efb5269f98c1170eee0baa2882d8a91d508fc7d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:34:06 +0200 Subject: [PATCH 07/20] Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.25 and remove custom providers --- .../UmbracoBuilder.ImageSharp.cs | 3 +- .../ImageProviders/FileInfoImageResolver.cs | 29 --------- .../FileProviderImageProvider.cs | 60 ------------------- .../ImageProviders/WebRootImageProvider.cs | 20 ------- .../Umbraco.Web.Common.csproj | 2 +- 5 files changed, 2 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs delete mode 100644 src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs delete mode 100644 src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index c05344d7d140..25dfe7e5cd03 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -15,7 +15,6 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; -using Umbraco.Cms.Web.Common.ImageProviders; namespace Umbraco.Extensions { @@ -74,7 +73,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build }; }) // Replace default image provider - .RemoveProvider().AddProvider() + .ClearProviders().AddProvider() // Configure cache options .Configure(options => { diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs deleted file mode 100644 index 4275cd137f09..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.FileProviders; -using SixLabors.ImageSharp.Web; -using SixLabors.ImageSharp.Web.Resolvers; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Provides means to manage image buffers from an instance. - /// - public class FileInfoImageResolver : IImageResolver - { - private readonly IFileInfo _fileInfo; - - /// - /// Initializes a new instance of the class. - /// - /// The file info. - public FileInfoImageResolver(IFileInfo fileInfo) - => _fileInfo = fileInfo; - - /// - public Task GetMetaDataAsync() => Task.FromResult(new ImageMetadata(_fileInfo.LastModified.UtcDateTime, _fileInfo.Length)); - - /// - public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); - } -} diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs deleted file mode 100644 index f41c6ba8502b..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.FileProviders; -using SixLabors.ImageSharp.Web; -using SixLabors.ImageSharp.Web.Providers; -using SixLabors.ImageSharp.Web.Resolvers; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Returns images from an abstraction. - /// - public class FileProviderImageProvider : IImageProvider - { - /// - /// The file provider abstraction. - /// - private readonly IFileProvider _fileProvider; - - /// - /// Contains various format helper methods based on the current configuration. - /// - private readonly FormatUtilities _formatUtilities; - - /// - /// Initializes a new instance of the class. - /// - /// The file provider. - /// Contains various format helper methods based on the current configuration. - public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities formatUtilities) - { - _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); - _formatUtilities = formatUtilities ?? throw new ArgumentNullException(nameof(formatUtilities)); - } - - /// - public ProcessingBehavior ProcessingBehavior { get; protected set; } = ProcessingBehavior.CommandOnly; - - /// - public Func Match { get; set; } = _ => true; - - /// - public bool IsValidRequest(HttpContext context) - => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); - - /// - public Task GetAsync(HttpContext context) - { - IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); - if (!fileInfo.Exists) - { - return Task.FromResult(null); - } - - return Task.FromResult(new FileInfoImageResolver(fileInfo)); - } - } -} diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs deleted file mode 100644 index 73605a6b7cc1..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using SixLabors.ImageSharp.Web; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Returns images from the web root file provider. - /// - public sealed class WebRootImageProvider : FileProviderImageProvider - { - /// - /// Initializes a new instance of the class. - /// - /// The web hosting environment. - /// Contains various format helper methods based on the current configuration. - public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) - : base(environment.WebRootFileProvider, formatUtilities) - { } - } -} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 545bb05308ae..2ad624e9e3be 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + From d35dfc6d68d40b39768ffe8c58ed5b90fb821c1a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:35:04 +0200 Subject: [PATCH 08/20] Make CropWebProcessor EXIF orientation-aware --- .../ImageProcessors/CropWebProcessor.cs | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 5d6d8062b594..519e524b196b 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Numerics; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; @@ -20,29 +22,25 @@ public class CropWebProcessor : IImageWebProcessor /// public const string Coordinates = "cc"; - /// + /// + /// The command constant for the resize orientation handling mode. + /// + public const string Orient = "orient"; + + /// public IEnumerable Commands { get; } = new[] { - Coordinates + Coordinates, + Orient }; - /// + /// public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) { - RectangleF? coordinates = GetCoordinates(commands, parser, culture); - if (coordinates != null) + Rectangle? cropRectangle = GetCropRectangle(image, commands, parser, culture); + if (cropRectangle.HasValue) { - // Convert the coordinates to a pixel based rectangle - int sourceWidth = image.Image.Width; - int sourceHeight = image.Image.Height; - int x = (int)MathF.Round(coordinates.Value.X * sourceWidth); - int y = (int)MathF.Round(coordinates.Value.Y * sourceHeight); - int width = (int)MathF.Round(coordinates.Value.Width * sourceWidth); - int height = (int)MathF.Round(coordinates.Value.Height * sourceHeight); - - var cropRectangle = new Rectangle(x, y, width, height); - - image.Image.Mutate(x => x.Crop(cropRectangle)); + image.Image.Mutate(x => x.Crop(cropRectangle.Value)); } return image; @@ -51,17 +49,44 @@ public FormattedImage Process(FormattedImage image, ILogger logger, CommandColle /// public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; - private static RectangleF? GetCoordinates(CommandCollection commands, CommandParser parser, CultureInfo culture) + private static Rectangle? GetCropRectangle(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) { float[] coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); - - if (coordinates.Length != 4) + if (coordinates.Length != 4 || + (coordinates[0] == 0 && coordinates[1] == 0 && coordinates[2] == 0 && coordinates[3] == 0)) { return null; } - // The right and bottom values are actually the distance from those sides, so convert them into real coordinates - return RectangleF.FromLTRB(coordinates[0], coordinates[1], 1 - coordinates[2], 1 - coordinates[3]); + // The right and bottom values are actually the distance from those sides, so convert them into real coordinates and transform to correct orientation + float left = Math.Clamp(coordinates[0], 0, 1); + float top = Math.Clamp(coordinates[1], 0, 1); + float right = Math.Clamp(1 - coordinates[2], 0, 1); + float bottom = Math.Clamp(1 - coordinates[3], 0, 1); + ushort orientation = GetExifOrientation(image, commands, parser, culture); + Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation); + Vector2 xy2 = ExifOrientationUtilities.Transform(new Vector2(right, bottom), Vector2.Zero, Vector2.One, orientation); + + // Scale points to a pixel based rectangle + Size size = image.Image.Size(); + int x = (int)MathF.Round(MathF.Min(xy1.X, xy2.X) * size.Width); + int y = (int)MathF.Round(MathF.Min(xy1.Y, xy2.Y) * size.Height); + int width = (int)MathF.Round(MathF.Max(xy1.X, xy2.X) * size.Width) - x; + int height = (int)MathF.Round(MathF.Max(xy1.Y, xy2.Y) * size.Height) - y; + + return new Rectangle(x, y, width, height); + } + + private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + { + if (commands.Contains(Orient) && !parser.ParseValue(commands.GetValueOrDefault(Orient), culture)) + { + return ExifOrientationMode.Unknown; + } + + image.TryGetExifOrientation(out ushort orientation); + + return orientation; } } } From ba491ec33f92916c614d7ae120d4df393a124d74 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:36:53 +0200 Subject: [PATCH 09/20] Improve width/height sanitization --- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 25dfe7e5cd03..3d2852799126 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -43,11 +43,15 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; } - uint width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - uint height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (width > imagingSettings.Resize.MaxWidth || height > imagingSettings.Resize.MaxHeight) + int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); + if (width <= 0 || width > imagingSettings.Resize.MaxWidth) { context.Commands.Remove(ResizeWebProcessor.Width); + } + + int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); + if (height <= 0 || height > imagingSettings.Resize.MaxHeight) + { context.Commands.Remove(ResizeWebProcessor.Height); } From 5b973a9c45de08a3339fd69bc68f2fde9bbcb893 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:37:54 +0200 Subject: [PATCH 10/20] Also use 'v' as cache buster value --- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 3d2852799126..b0c9c48e7b21 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -62,7 +62,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build options.OnPrepareResponseAsync = context => { // Change Cache-Control header when cache buster value is present - if (context.Request.Query.ContainsKey("rnd")) + if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) { var headers = context.Response.GetTypedHeaders(); From 2cbef09aaa77de39abdcc5d64251ed6988973a11 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:38:23 +0200 Subject: [PATCH 11/20] Add WebP to supported image file types --- src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 990b3c61cbae..2e109fe31009 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs @@ -23,7 +23,7 @@ public class ContentImagingSettings } }; - internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif"; + internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; /// /// Gets or sets a value for the collection of accepted image file extensions. From e7f74dcb21732aad7c97652125fc633815483c70 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 5 Apr 2022 16:18:59 +0200 Subject: [PATCH 12/20] Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.27 and fix test --- .../Umbraco.Web.Common.csproj | 2 +- .../ImageProcessors/CropWebProcessorTests.cs | 31 ++----------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 2ad624e9e3be..3ab3c56e32ce 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index a5811c068165..3bad2051b82c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Reflection; using NUnit.Framework; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -24,8 +23,8 @@ public void CropWebProcessor_CropsImage() { var converters = new List { - CreateArrayConverterOfFloat(), - CreateSimpleCommandConverterOfFloat(), + new ArrayConverter(), + CreateSimpleCommandConverterOfFloat() }; var parser = new CommandParser(converters); @@ -37,22 +36,13 @@ public void CropWebProcessor_CropsImage() }; using var image = new Image(50, 80); - using FormattedImage formatted = CreateFormattedImage(image, PngFormat.Instance); + using FormattedImage formatted = new FormattedImage(image, PngFormat.Instance); new CropWebProcessor().Process(formatted, null, commands, parser, culture); Assert.AreEqual(40, image.Width); // Cropped 5 pixels from each side. Assert.AreEqual(32, image.Height); // Cropped 16 pixels from the top and 32 from the bottom. } - private static ICommandConverter CreateArrayConverterOfFloat() - { - // ImageSharp.Web's ArrayConverter is internal, so we need to use reflection to instantiate. - var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.ArrayConverter`1, SixLabors.ImageSharp.Web"); - Type[] typeArgs = { typeof(float) }; - Type genericType = type.MakeGenericType(typeArgs); - return (ICommandConverter)Activator.CreateInstance(genericType); - } - private static ICommandConverter CreateSimpleCommandConverterOfFloat() { // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. @@ -61,20 +51,5 @@ private static ICommandConverter CreateSimpleCommandConverterOfFloat() Type genericType = type.MakeGenericType(typeArgs); return (ICommandConverter)Activator.CreateInstance(genericType); } - - private FormattedImage CreateFormattedImage(Image image, PngFormat format) - { - // Again, the constructor of FormattedImage useful for tests is internal, so we need to use reflection. - Type type = typeof(FormattedImage); - var instance = type.Assembly.CreateInstance( - type.FullName, - false, - BindingFlags.Instance | BindingFlags.NonPublic, - null, - new object[] { image, format }, - null, - null); - return (FormattedImage)instance; - } } } From 70e2437db0ff7490a2c348f7a4c0fb844775612e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sat, 9 Apr 2022 13:24:31 +0200 Subject: [PATCH 13/20] Fix rounding error and add test cases --- .../ImageProcessors/CropWebProcessor.cs | 10 ++-- .../ImageProcessors/CropWebProcessorTests.cs | 46 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 519e524b196b..85ecf2d844d2 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -69,12 +69,12 @@ public FormattedImage Process(FormattedImage image, ILogger logger, CommandColle // Scale points to a pixel based rectangle Size size = image.Image.Size(); - int x = (int)MathF.Round(MathF.Min(xy1.X, xy2.X) * size.Width); - int y = (int)MathF.Round(MathF.Min(xy1.Y, xy2.Y) * size.Height); - int width = (int)MathF.Round(MathF.Max(xy1.X, xy2.X) * size.Width) - x; - int height = (int)MathF.Round(MathF.Max(xy1.Y, xy2.Y) * size.Height) - y; - return new Rectangle(x, y, width, height); + return Rectangle.Round(RectangleF.FromLTRB( + MathF.Min(xy1.X, xy2.X) * size.Width, + MathF.Min(xy1.Y, xy2.Y) * size.Height, + MathF.Max(xy1.X, xy2.X) * size.Width, + MathF.Max(xy1.Y, xy2.Y) * size.Height)); } private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index 3bad2051b82c..d9892177d02c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -2,8 +2,8 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Globalization; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Commands.Converters; +using SixLabors.ImageSharp.Web.Middleware; using Umbraco.Cms.Web.Common.ImageProcessors; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.ImageProcessors @@ -19,36 +20,45 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.ImageProcessors public class CropWebProcessorTests { [Test] - public void CropWebProcessor_CropsImage() + // Coordinates are percentages to crop from the left, top, right and bottom sides + [TestCase("0,0,0,0", 50, 90)] + [TestCase("0.1,0.0,0.0,0.0", 45, 90)] + [TestCase("0.0,0.1,0.0,0.0", 50, 81)] + [TestCase("0.0,0.0,0.1,0.0", 45, 90)] + [TestCase("0.0,0.0,0.0,0.1", 50, 81)] + [TestCase("0.1,0.0,0.1,0.0", 40, 90)] + [TestCase("0.0,0.1,0.0,0.1", 50, 72)] + [TestCase("0.1,0.1,0.1,0.1", 40, 72)] + [TestCase("0.25,0.25,0.25,0.25", 25, 45)] + public void CropWebProcessor_CropsImage(string coordinates, int width, int height) { - var converters = new List - { - new ArrayConverter(), - CreateSimpleCommandConverterOfFloat() - }; - - var parser = new CommandParser(converters); - CultureInfo culture = CultureInfo.InvariantCulture; + using var image = new Image(50, 90); + using var formattedImage = new FormattedImage(image, PngFormat.Instance); + var logger = new NullLogger(); var commands = new CommandCollection { - { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom + { CropWebProcessor.Coordinates, coordinates }, }; + var parser = new CommandParser(new[] + { + new ArrayConverter(), + CreateSimpleCommandConverterOfFloat() + }); + var culture = CultureInfo.InvariantCulture; - using var image = new Image(50, 80); - using FormattedImage formatted = new FormattedImage(image, PngFormat.Instance); - new CropWebProcessor().Process(formatted, null, commands, parser, culture); + new CropWebProcessor().Process(formattedImage, logger, commands, parser, culture); - Assert.AreEqual(40, image.Width); // Cropped 5 pixels from each side. - Assert.AreEqual(32, image.Height); // Cropped 16 pixels from the top and 32 from the bottom. + Assert.AreEqual(width, image.Width); + Assert.AreEqual(height, image.Height); } private static ICommandConverter CreateSimpleCommandConverterOfFloat() { // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); - Type[] typeArgs = { typeof(float) }; - Type genericType = type.MakeGenericType(typeArgs); + var genericType = type.MakeGenericType(typeof(float)); + return (ICommandConverter)Activator.CreateInstance(genericType); } } From 67b8dbc003f5ecea3f0129bb82dbcf2b4cb3ffc6 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 25 Apr 2022 10:44:15 +0200 Subject: [PATCH 14/20] Update to newest and stable releases --- NuGet.config | 6 ------ src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 NuGet.config diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index 6aa8697b8fe9..000000000000 --- a/NuGet.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 40cda9903844..73f748827692 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 3ab3c56e32ce..64d0c9aa8e5b 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + From cac54022d68512ace0891f85c553d8b4bb58fb15 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 10:56:53 +0200 Subject: [PATCH 15/20] Move ImageSharpImageUrlGenerator to Umbraco.Web.Common --- .../UmbracoBuilder.CoreServices.cs | 1 - .../Media/ImageSharpImageUrlGenerator.cs | 107 ------------------ .../UmbracoBuilder.ImageSharp.cs | 6 +- .../Media/ImageSharpImageUrlGenerator.cs | 100 ++++++++++++++++ .../Media/ImageSharpImageUrlGeneratorTests.cs | 4 +- 5 files changed, 106 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs create mode 100644 src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs rename tests/Umbraco.Tests.UnitTests/{Umbraco.Infrastructure => Umbraco.Web.Common}/Media/ImageSharpImageUrlGeneratorTests.cs (98%) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 2a429fc6c560..44e898fcc823 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -200,7 +200,6 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde // Add default ImageSharp configuration and service implementations builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs deleted file mode 100644 index cfca16601ed4..000000000000 --- a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using SixLabors.ImageSharp; -using Umbraco.Cms.Core.Media; -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.Media -{ - /// - /// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp. - /// - /// - public class ImageSharpImageUrlGenerator : IImageUrlGenerator - { - /// - public IEnumerable SupportedImageFileTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The supported image file types/extensions. - /// - /// This constructor is only used for testing. - /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; - - /// - public string? GetImageUrl(ImageUrlGenerationOptions options) - { - if (options == null) - { - return null; - } - - var imageUrl = new StringBuilder(options.ImageUrl); - - bool queryStringHasStarted = false; - void AppendQueryString(string value) - { - imageUrl.Append(queryStringHasStarted ? '&' : '?'); - queryStringHasStarted = true; - - imageUrl.Append(value); - } - void AddQueryString(string key, params IConvertible[] values) - => AppendQueryString(key + '=' + string.Join(",", values.Select(x => x.ToString(CultureInfo.InvariantCulture)))); - - if (options.Crop != null) - { - AddQueryString("cc", options.Crop.Left, options.Crop.Top, options.Crop.Right, options.Crop.Bottom); - } - - if (options.FocalPoint != null) - { - AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top); - } - - if (options.ImageCropMode.HasValue) - { - AddQueryString("rmode", options.ImageCropMode.Value.ToString().ToLowerInvariant()); - } - - if (options.ImageCropAnchor.HasValue) - { - AddQueryString("ranchor", options.ImageCropAnchor.Value.ToString().ToLowerInvariant()); - } - - if (options.Width.HasValue) - { - AddQueryString("width", options.Width.Value); - } - - if (options.Height.HasValue) - { - AddQueryString("height", options.Height.Value); - } - - if (options.Quality.HasValue) - { - AddQueryString("quality", options.Quality.Value); - } - - if (string.IsNullOrWhiteSpace(options.FurtherOptions) == false) - { - AppendQueryString(options.FurtherOptions.TrimStart('?', '&')); - } - - if (string.IsNullOrWhiteSpace(options.CacheBusterValue) == false) - { - AddQueryString("rnd", options.CacheBusterValue); - } - - return imageUrl.ToString(); - } - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index dbb06b1cb932..a1a8de5bb048 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp.Web.Caching; @@ -13,9 +12,10 @@ using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Media; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; +using Umbraco.Cms.Web.Common.Media; namespace Umbraco.Extensions { @@ -26,6 +26,8 @@ public static partial class UmbracoBuilderExtensions /// public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { + builder.Services.AddSingleton(); + ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) .Get() ?? new ImagingSettings(); diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs new file mode 100644 index 000000000000..02aed2ee9636 --- /dev/null +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web.Processors; +using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Web.Common.ImageProcessors; +using static Umbraco.Cms.Core.Models.ImageUrlGenerationOptions; + +namespace Umbraco.Cms.Web.Common.Media +{ + /// + /// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp. + /// + /// + public class ImageSharpImageUrlGenerator : IImageUrlGenerator + { + /// + public IEnumerable SupportedImageFileTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + public ImageSharpImageUrlGenerator(Configuration configuration) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The supported image file types/extensions. + /// + /// This constructor is only used for testing. + /// + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; + + /// + public string? GetImageUrl(ImageUrlGenerationOptions options) + { + if (options == null || string.IsNullOrEmpty(options.ImageUrl)) + { + return null; + } + + var queryString = new Dictionary(); + + if (options.Crop is CropCoordinates crop) + { + queryString.Add(CropWebProcessor.Coordinates, FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}")); + } + + if (options.FocalPoint is FocalPointPosition focalPoint) + { + queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{focalPoint.Left},{focalPoint.Top}")); + } + + if (options.ImageCropMode is ImageCropMode imageCropMode) + { + queryString.Add(ResizeWebProcessor.Mode, imageCropMode.ToString().ToLowerInvariant()); + } + + if (options.ImageCropAnchor is ImageCropAnchor imageCropAnchor) + { + queryString.Add(ResizeWebProcessor.Anchor, imageCropAnchor.ToString().ToLowerInvariant()); + } + + if (options.Width is int width) + { + queryString.Add(ResizeWebProcessor.Width, width.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Height is int height) + { + queryString.Add(ResizeWebProcessor.Height, height.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Quality is int quality) + { + queryString.Add(QualityWebProcessor.Quality, quality.ToString(CultureInfo.InvariantCulture)); + } + + foreach (KeyValuePair kvp in QueryHelpers.ParseQuery(options.FurtherOptions)) + { + queryString.Add(kvp.Key, kvp.Value); + } + + if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrWhiteSpace(cacheBusterValue)) + { + queryString.Add("rnd", cacheBusterValue); + } + + return QueryHelpers.AddQueryString(options.ImageUrl, queryString); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs similarity index 98% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs index a531aa6bbd01..3098a523508f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -3,9 +3,9 @@ using NUnit.Framework; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Infrastructure.Media; +using Umbraco.Cms.Web.Common.Media; -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Media { [TestFixture] public class ImageSharpImageUrlGeneratorTests From 3cae534f264a301ded64982aa3e3544829a92c09 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 11:11:29 +0200 Subject: [PATCH 16/20] Use IConfigureOptions to configure ImageSharp options --- .../ConfigureImageSharpMiddlewareOptions.cs | 88 +++++++++++++++++++ ...ConfigurePhysicalFileSystemCacheOptions.cs | 36 ++++++++ .../ImageSharpConfigurationOptions.cs | 30 ------- .../UmbracoBuilder.ImageSharp.cs | 78 ++-------------- 4 files changed, 132 insertions(+), 100 deletions(-) create mode 100644 src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs create mode 100644 src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs delete mode 100644 src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs new file mode 100644 index 000000000000..69b37cd7da2d --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Middleware; +using SixLabors.ImageSharp.Web.Processors; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// Configures the ImageSharp middleware options. + /// + /// + public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions + { + private readonly Configuration _configuration; + private readonly ImagingSettings _imagingSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + /// The Umbraco imaging settings. + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) + { + _configuration = configuration; + _imagingSettings = imagingSettings.Value; + } + + /// + public void Configure(ImageSharpMiddlewareOptions options) + { + options.Configuration = _configuration; + + options.BrowserMaxAge = _imagingSettings.Cache.BrowserMaxAge; + options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; + options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; + + // Use configurable maximum width and height + options.OnParseCommandsAsync = context => + { + if (context.Commands.Count == 0) + { + return Task.CompletedTask; + } + + int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); + if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) + { + context.Commands.Remove(ResizeWebProcessor.Width); + } + + int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); + if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) + { + context.Commands.Remove(ResizeWebProcessor.Height); + } + + return Task.CompletedTask; + }; + + // Change Cache-Control header when cache buster value is present + options.OnPrepareResponseAsync = context => + { + if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) + { + ResponseHeaders headers = context.Response.GetTypedHeaders(); + + CacheControlHeaderValue cacheControl = headers.CacheControl ?? new CacheControlHeaderValue() + { + Public = true + }; + cacheControl.MustRevalidate = false; // ImageSharp enables this by default + cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); + + headers.CacheControl = cacheControl; + } + + return Task.CompletedTask; + }; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs new file mode 100644 index 000000000000..16f247618957 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using SixLabors.ImageSharp.Web.Caching; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Extensions; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// Configures the ImageSharp physical file system cache options. + /// + /// + public sealed class ConfigurePhysicalFileSystemCacheOptions : IConfigureOptions + { + private readonly ImagingSettings _imagingSettings; + private readonly IHostEnvironment _hostEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// The Umbraco imaging settings. + /// The host environment. + public ConfigurePhysicalFileSystemCacheOptions(IOptions imagingSettings, IHostEnvironment hostEnvironment) + { + _imagingSettings = imagingSettings.Value; + _hostEnvironment = hostEnvironment; + } + + /// + public void Configure(PhysicalFileSystemCacheOptions options) + { + options.CacheFolder = _hostEnvironment.MapPathContentRoot(_imagingSettings.Cache.CacheFolder); + options.CacheFolderDepth = _imagingSettings.Cache.CacheFolderDepth; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs deleted file mode 100644 index f8897e522cd6..000000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Options; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Web.Middleware; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - /// - /// Configures the ImageSharp middleware options to use the registered configuration. - /// - /// - public sealed class ImageSharpConfigurationOptions : IConfigureOptions - { - /// - /// The ImageSharp configuration. - /// - private readonly Configuration _configuration; - - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration; - - /// - /// Invoked to configure an instance. - /// - /// The options instance to configure. - public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration; - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index a1a8de5bb048..cfba33d0ae7b 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -1,16 +1,9 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp.Web.Caching; -using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware; -using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -28,73 +21,18 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build { builder.Services.AddSingleton(); - ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) - .Get() ?? new ImagingSettings(); - - builder.Services.AddImageSharp(options => - { - // options.Configuration is set using ImageSharpConfigurationOptions below - options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CacheHashLength = imagingSettings.Cache.CacheHashLength; - - // Use configurable maximum width and height (overwrite ImageSharps default) - options.OnParseCommandsAsync = context => - { - if (context.Commands.Count == 0) - { - return Task.CompletedTask; - } - - int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - if (width <= 0 || width > imagingSettings.Resize.MaxWidth) - { - context.Commands.Remove(ResizeWebProcessor.Width); - } - - int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (height <= 0 || height > imagingSettings.Resize.MaxHeight) - { - context.Commands.Remove(ResizeWebProcessor.Height); - } - - return Task.CompletedTask; - }; - options.OnBeforeSaveAsync = _ => Task.CompletedTask; - options.OnProcessedAsync = _ => Task.CompletedTask; - options.OnPrepareResponseAsync = context => - { - // Change Cache-Control header when cache buster value is present - if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) - { - var headers = context.Response.GetTypedHeaders(); - - var cacheControl = headers.CacheControl; - if (cacheControl is not null) - { - cacheControl.MustRevalidate = false; - cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); - } - - headers.CacheControl = cacheControl; - } - - return Task.CompletedTask; - }; - }) + builder.Services.AddImageSharp() // Replace default image provider - .ClearProviders().AddProvider() - // Configure cache options - .Configure(options => - { - options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder); - options.CacheFolderDepth = imagingSettings.Cache.CacheFolderDepth; - }) + .ClearProviders() + .AddProvider() // Add custom processors .AddProcessor(); - // Configure middleware to use the registered/shared ImageSharp configuration - builder.Services.AddTransient, ImageSharpConfigurationOptions>(); + // Configure middleware + builder.Services.AddTransient, ConfigureImageSharpMiddlewareOptions>(); + + // Configure cache options + builder.Services.AddTransient, ConfigurePhysicalFileSystemCacheOptions>(); return builder.Services; } From 10eb181c4fa5e68f525b04c48230d809d20d5f6b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 11:53:27 +0200 Subject: [PATCH 17/20] Implement IEquatable on ImageUrlGenerationOptions classes --- .../Models/ImageUrlGenerationOptions.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 855c7c00bcd9..876b2bfddb7e 100644 --- a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -1,9 +1,12 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Cms.Core.Models { /// /// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. /// - public class ImageUrlGenerationOptions + public class ImageUrlGenerationOptions : IEquatable { public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; @@ -27,10 +30,43 @@ public class ImageUrlGenerationOptions public string? FurtherOptions { get; set; } + public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); + + public bool Equals(ImageUrlGenerationOptions? other) + => other != null && + ImageUrl == other.ImageUrl && + Width == other.Width && + Height == other.Height && + Quality == other.Quality && + ImageCropMode == other.ImageCropMode && + ImageCropAnchor == other.ImageCropAnchor && + EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && + EqualityComparer.Default.Equals(Crop, other.Crop) && + CacheBusterValue == other.CacheBusterValue && + FurtherOptions == other.FurtherOptions; + + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(ImageUrl); + hash.Add(Width); + hash.Add(Height); + hash.Add(Quality); + hash.Add(ImageCropMode); + hash.Add(ImageCropAnchor); + hash.Add(FocalPoint); + hash.Add(Crop); + hash.Add(CacheBusterValue); + hash.Add(FurtherOptions); + + return hash.ToHashCode(); + } + /// /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0. /// - public class FocalPointPosition + public class FocalPointPosition : IEquatable { public FocalPointPosition(decimal left, decimal top) { @@ -41,12 +77,21 @@ public FocalPointPosition(decimal left, decimal top) public decimal Left { get; } public decimal Top { get; } + + public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + + public bool Equals(FocalPointPosition? other) + => other != null && + Left == other.Left && + Top == other.Top; + + public override int GetHashCode() => HashCode.Combine(Left, Top); } /// /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, typically a percentage between 0.0 and 1.0. /// - public class CropCoordinates + public class CropCoordinates : IEquatable { public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) { @@ -63,6 +108,17 @@ public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) public decimal Right { get; } public decimal Bottom { get; } + + public override bool Equals(object? obj) => Equals(obj as CropCoordinates); + + public bool Equals(CropCoordinates? other) + => other != null && + Left == other.Left && + Top == other.Top && + Right == other.Right && + Bottom == other.Bottom; + + public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); } } } From 2de55205bef8935e457146849453465aacb379df Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 12:10:51 +0200 Subject: [PATCH 18/20] Fix empty/null values in image URL generation and corresponding tests --- .../Media/ImageSharpImageUrlGenerator.cs | 2 +- .../Media/ImageSharpImageUrlGeneratorTests.cs | 44 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 02aed2ee9636..1addc76abb93 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -42,7 +42,7 @@ public ImageSharpImageUrlGenerator(Configuration configuration) /// public string? GetImageUrl(ImageUrlGenerationOptions options) { - if (options == null || string.IsNullOrEmpty(options.ImageUrl)) + if (options?.ImageUrl == null) { return null; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs index 3098a523508f..f529d17bd0ad 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -17,60 +17,70 @@ public class ImageSharpImageUrlGeneratorTests private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0]); [Test] - public void GetCropUrl_CropAliasTest() + public void GetImageUrl_CropAliasTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Crop = s_crop, Width = 100, Height = 100 }); Assert.AreEqual(MediaPath + "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100", urlString); } [Test] - public void GetCropUrl_WidthHeightTest() + public void GetImageUrl_WidthHeightTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 200, Height = 300 }); Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300", urlString); } [Test] - public void GetCropUrl_FocalPointTest() + public void GetImageUrl_FocalPointTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 100, Height = 100 }); Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=100&height=100", urlString); } [Test] - public void GetCropUrlFurtherOptionsTest() + public void GetImageUrlFurtherOptionsTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 200, Height = 300, FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff" }); - Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); + Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff", urlString); } /// /// Test that if options is null, the generated image URL is also null. /// [Test] - public void GetCropUrlNullTest() + public void GetImageUrlNullOptionsTest() { var urlString = s_generator.GetImageUrl(null); Assert.AreEqual(null, urlString); } /// - /// Test that if the image URL is null, the generated image URL is empty. + /// Test that if the image URL is null, the generated image URL is also null. /// [Test] - public void GetCropUrlEmptyTest() + public void GetImageUrlNullTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null)); + Assert.AreEqual(null, urlString); + } + + /// + /// Test that if the image URL is empty, the generated image URL is empty. + /// + [Test] + public void GetImageUrlEmptyTest() + { + var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)); Assert.AreEqual(string.Empty, urlString); } /// - /// Test the GetCropUrl method on the ImageCropDataSet Model + /// Test the GetImageUrl method on the ImageCropDataSet Model /// [Test] public void GetBaseCropUrlFromModelTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null) { Crop = s_crop, Width = 100, Height = 100 }); + var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { Crop = s_crop, Width = 100, Height = 100 }); Assert.AreEqual("?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100", urlString); } @@ -78,7 +88,7 @@ public void GetBaseCropUrlFromModelTest() /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop /// [Test] - public void GetCropUrl_SpecifiedCropModeTest() + public void GetImageUrl_SpecifiedCropModeTest() { var urlStringMin = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Min, Width = 300, Height = 150 }); var urlStringBoxPad = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.BoxPad, Width = 300, Height = 150 }); @@ -97,7 +107,7 @@ public void GetCropUrl_SpecifiedCropModeTest() /// Test for upload property type /// [Test] - public void GetCropUrl_UploadTypeTest() + public void GetImageUrl_UploadTypeTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Crop, ImageCropAnchor = ImageCropAnchor.Center, Width = 100, Height = 270 }); Assert.AreEqual(MediaPath + "?rmode=crop&ranchor=center&width=100&height=270", urlString); @@ -107,7 +117,7 @@ public void GetCropUrl_UploadTypeTest() /// Test for preferFocalPoint when focal point is centered /// [Test] - public void GetCropUrl_PreferFocalPointCenter() + public void GetImageUrl_PreferFocalPointCenter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 300, Height = 150 }); Assert.AreEqual(MediaPath + "?width=300&height=150", urlString); @@ -117,7 +127,7 @@ public void GetCropUrl_PreferFocalPointCenter() /// Test to check if crop ratio is ignored if useCropDimensions is true /// [Test] - public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() + public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus2, Width = 270, Height = 161 }); Assert.AreEqual(MediaPath + "?rxy=0.4275,0.41&width=270&height=161", urlString); @@ -127,7 +137,7 @@ public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() /// Test to check result when only a width parameter is passed, effectivly a resize only /// [Test] - public void GetCropUrl_WidthOnlyParameter() + public void GetImageUrl_WidthOnlyParameter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 200 }); Assert.AreEqual(MediaPath + "?width=200", urlString); @@ -137,7 +147,7 @@ public void GetCropUrl_WidthOnlyParameter() /// Test to check result when only a height parameter is passed, effectivly a resize only /// [Test] - public void GetCropUrl_HeightOnlyParameter() + public void GetImageUrl_HeightOnlyParameter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Height = 200 }); Assert.AreEqual(MediaPath + "?height=200", urlString); @@ -147,7 +157,7 @@ public void GetCropUrl_HeightOnlyParameter() /// Test to check result when using a background color with padding /// [Test] - public void GetCropUrl_BackgroundColorParameter() + public void GetImageUrl_BackgroundColorParameter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Pad, Width = 400, Height = 400, FurtherOptions = "&bgcolor=fff" }); Assert.AreEqual(MediaPath + "?rmode=pad&width=400&height=400&bgcolor=fff", urlString); From 5898b0c87ae44747895fc72c7b64e0d582a04b56 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 13:18:05 +0200 Subject: [PATCH 19/20] Use IsSupportedImageFormat extension method --- src/Umbraco.Web.BackOffice/Controllers/MediaController.cs | 4 ++-- src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 6f43e90acf62..ce0bb9846bca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -840,7 +840,7 @@ public async Task PostAddFile([FromForm] string path, [FromForm] { var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd(); var safeFileName = fileName.ToSafeFileName(ShortStringHelper); - var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant(); if (!_contentSettings.IsFileAllowedForUpload(ext)) { @@ -885,7 +885,7 @@ public async Task PostAddFile([FromForm] string path, [FromForm] } // If media type is still File then let's check if it's an image. - if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext)) + if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.IsSupportedImageFormat(ext)) { mediaTypeAlias = Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index cc4bc82ad628..1b067e71c207 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -75,9 +75,9 @@ public async Task UploadImage(List file) // var file = result.FileData[0]; var fileName = formFile.FileName.Trim(new[] { '\"' }).TrimEnd(); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); - var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant(); - if (_contentSettings.IsFileAllowedForUpload(ext) == false || _imageUrlGenerator.SupportedImageFileTypes.Contains(ext) == false) + if (_contentSettings.IsFileAllowedForUpload(ext) == false || _imageUrlGenerator.IsSupportedImageFormat(ext) == false) { // Throw some error - to say can't upload this IMG type return new UmbracoProblemResult("This is not an image filetype extension that is approved", HttpStatusCode.BadRequest); From 5cb9a9b1341a713f730e33aac3fd067de4a840af Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 13:41:08 +0200 Subject: [PATCH 20/20] Remove unneeded reflection --- .../ImageProcessors/CropWebProcessorTests.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index d9892177d02c..7a16ff9abfca 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; @@ -40,10 +39,10 @@ public void CropWebProcessor_CropsImage(string coordinates, int width, int heigh { { CropWebProcessor.Coordinates, coordinates }, }; - var parser = new CommandParser(new[] + var parser = new CommandParser(new ICommandConverter[] { new ArrayConverter(), - CreateSimpleCommandConverterOfFloat() + new SimpleCommandConverter() }); var culture = CultureInfo.InvariantCulture; @@ -52,14 +51,5 @@ public void CropWebProcessor_CropsImage(string coordinates, int width, int heigh Assert.AreEqual(width, image.Width); Assert.AreEqual(height, image.Height); } - - private static ICommandConverter CreateSimpleCommandConverterOfFloat() - { - // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. - var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); - var genericType = type.MakeGenericType(typeof(float)); - - return (ICommandConverter)Activator.CreateInstance(genericType); - } } }