From 1a82e0854a79970cc4100649cf078ab95e581ac2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 29 Apr 2022 13:16:24 +0200 Subject: [PATCH] v10: Update to ImageSharp v2 (#12185) * Update to ImageSharp 2.1.0 and ImageSharp.Web 2.0.0-alpha.0.23 * Rename CachedNameLength to CacheHashLength and add CacheFolderDepth setting * Replace PhysicalFileSystemProvider with WebRootImageProvider * Support EXIF-orientation in image dimention extractor * Remove virtual methods on FileProviderImageProvider * Simplify FileInfoImageResolver * Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.25 and remove custom providers * Make CropWebProcessor EXIF orientation-aware * Improve width/height sanitization * Also use 'v' as cache buster value * Add WebP to supported image file types * Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.27 and fix test * Fix rounding error and add test cases * Update to newest and stable releases * Move ImageSharpImageUrlGenerator to Umbraco.Web.Common * Use IConfigureOptions to configure ImageSharp options * Implement IEquatable on ImageUrlGenerationOptions classes * Fix empty/null values in image URL generation and corresponding tests * Use IsSupportedImageFormat extension method * Remove unneeded reflection --- .../Models/ContentImagingSettings.cs | 2 +- .../Models/ImagingCacheSettings.cs | 19 +++- .../Models/ImageUrlGenerationOptions.cs | 62 +++++++++- .../UmbracoBuilder.CoreServices.cs | 1 - .../Media/ImageSharpDimensionExtractor.cs | 34 +++++- .../Media/ImageSharpImageUrlGenerator.cs | 107 ------------------ .../Umbraco.Infrastructure.csproj | 2 +- .../Controllers/MediaController.cs | 4 +- .../Controllers/TinyMceController.cs | 6 +- .../ConfigureImageSharpMiddlewareOptions.cs | 88 ++++++++++++++ ...ConfigurePhysicalFileSystemCacheOptions.cs | 36 ++++++ .../ImageSharpConfigurationOptions.cs | 30 ----- .../UmbracoBuilder.ImageSharp.cs | 79 +++---------- .../ImageProcessors/CropWebProcessor.cs | 72 ++++++++---- .../Media/ImageSharpImageUrlGenerator.cs | 100 ++++++++++++++++ .../Umbraco.Web.Common.csproj | 82 +++++++------- .../ImageProcessors/CropWebProcessorTests.cs | 79 +++++-------- .../Media/ImageSharpImageUrlGeneratorTests.cs | 48 ++++---- 18 files changed, 494 insertions(+), 357 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs 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 create mode 100644 src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs rename tests/Umbraco.Tests.UnitTests/{Umbraco.Infrastructure => Umbraco.Web.Common}/Media/ImageSharpImageUrlGeneratorTests.cs (83%) diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 990b3c61cb..2e109fe310 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Configuration.Models } }; - 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. diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs index cd7d2fda1b..b3bdddc211 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs @@ -14,8 +14,9 @@ namespace Umbraco.Cms.Core.Configuration.Models { 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 @@ namespace Umbraco.Cms.Core.Configuration.Models 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.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 855c7c00bc..876b2bfddb 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 @@ namespace Umbraco.Cms.Core.Models 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 @@ namespace Umbraco.Cms.Core.Models 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 @@ namespace Umbraco.Cms.Core.Models 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); } } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index a2ab3ca861..546b216aab 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -201,7 +201,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection // 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/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 822c639064..2afacbdfbc 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 @@ namespace Umbraco.Cms.Infrastructure.Media 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; + } } } diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs deleted file mode 100644 index cfca16601e..0000000000 --- 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.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 006e651706..8affd81c91 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 6f43e90acf..ce0bb9846b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -840,7 +840,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { 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 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } // 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 cc4bc82ad6..1b067e71c2 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 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // 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); diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs new file mode 100644 index 0000000000..69b37cd7da --- /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 0000000000..16f2476189 --- /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 f8897e522c..0000000000 --- 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 acf3e903b9..cfba33d0ae 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -1,20 +1,14 @@ -using System.Threading.Tasks; -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; -using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware; -using SixLabors.ImageSharp.Web.Processors; -using Umbraco.Cms.Core.Configuration.Models; +using SixLabors.ImageSharp.Web.Providers; 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 { @@ -25,65 +19,20 @@ namespace Umbraco.Extensions /// public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { - ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) - .Get() ?? new ImagingSettings(); + builder.Services.AddSingleton(); - builder.Services.AddImageSharp(options => - { - // options.Configuration is set using ImageSharpConfigurationOptions below - options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CachedNameLength = imagingSettings.Cache.CachedNameLength; + builder.Services.AddImageSharp() + // Replace default image provider + .ClearProviders() + .AddProvider() + // Add custom processors + .AddProcessor(); - // Use configurable maximum width and height (overwrite ImageSharps default) - options.OnParseCommandsAsync = context => - { - if (context.Commands.Count == 0) - { - return Task.CompletedTask; - } + // Configure middleware + builder.Services.AddTransient, ConfigureImageSharpMiddlewareOptions>(); - 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) - { - context.Commands.Remove(ResizeWebProcessor.Width); - 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")) - { - 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; - }; - }).AddProcessor(); - - builder.Services.AddOptions() - .Configure((opt, hostEnvironment) => - { - opt.CacheFolder = hostEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder); - }); - - // Configure middleware to use the registered/shared ImageSharp configuration - builder.Services.AddTransient, ImageSharpConfigurationOptions>(); + // Configure cache options + builder.Services.AddTransient, ConfigurePhysicalFileSystemCacheOptions>(); return builder.Services; } diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 7b3cc817f2..85ecf2d844 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,45 +22,71 @@ namespace Umbraco.Cms.Web.Common.ImageProcessors /// 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, 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) + 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; } - private static RectangleF? GetCoordinates(IDictionary commands, CommandParser parser, CultureInfo culture) + /// + public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; + + 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(); + + 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) + { + if (commands.Contains(Orient) && !parser.ParseValue(commands.GetValueOrDefault(Orient), culture)) + { + return ExifOrientationMode.Unknown; + } + + image.TryGetExifOrientation(out ushort orientation); + + return orientation; } } } diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs new file mode 100644 index 0000000000..1addc76abb --- /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?.ImageUrl == null) + { + 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/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 54a693b60b..d1f34f2079 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -1,52 +1,46 @@ - - net6.0 - Library - Umbraco.Cms.Web.Common - Umbraco.Cms.Web.Common - Umbraco CMS Web - Contains the Web assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco - enable - + + net6.0 + Library + Umbraco.Cms.Web.Common + Umbraco.Cms.Web.Common + Umbraco CMS Web + Contains the Web assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco + enable + - - bin\Release\Umbraco.Web.Common.xml - + + bin\Release\Umbraco.Web.Common.xml + - - - + + + - - - - - - + + + + + + - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - all - - + + + + + + + + + + + + - - - <_Parameter1>Umbraco.Tests.UnitTests - - + + + <_Parameter1>Umbraco.Tests.UnitTests + + 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 2c508d97d2..7a16ff9abf 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -1,10 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Reflection; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -12,6 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; 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 @@ -20,61 +19,37 @@ 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 + using var image = new Image(50, 90); + using var formattedImage = new FormattedImage(image, PngFormat.Instance); + + var logger = new NullLogger(); + var commands = new CommandCollection { - CreateArrayConverterOfFloat(), - CreateSimpleCommandConverterOfFloat(), + { CropWebProcessor.Coordinates, coordinates }, }; - - var parser = new CommandParser(converters); - CultureInfo culture = CultureInfo.InvariantCulture; - - var commands = new Dictionary + var parser = new CommandParser(new ICommandConverter[] { - { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom - }; + new ArrayConverter(), + new SimpleCommandConverter() + }); + var culture = CultureInfo.InvariantCulture; - using var image = new Image(50, 80); - using FormattedImage formatted = CreateFormattedImage(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. - } - - 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. - var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); - Type[] typeArgs = { typeof(float) }; - 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; + Assert.AreEqual(width, image.Width); + Assert.AreEqual(height, image.Height); } } } 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 83% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs index a531aa6bbd..f529d17bd0 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 @@ -17,60 +17,70 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media /// 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);