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
This commit is contained in:
Ronald Barendse
2022-04-29 13:16:24 +02:00
committed by GitHub
parent 6b5bc7cebd
commit 1a82e0854a
18 changed files with 494 additions and 357 deletions

View File

@@ -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";
/// <summary>
/// Gets or sets a value for the collection of accepted image file extensions.

View File

@@ -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";
/// <summary>
/// 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);
/// <summary>
/// Gets or sets a value for length of the cached name.
/// Gets or sets a value for the image cache hash length.
/// </summary>
[DefaultValue(StaticCachedNameLength)]
public uint CachedNameLength { get; set; } = StaticCachedNameLength;
[DefaultValue(StaticCacheHashLength)]
public uint CacheHashLength { get; set; } = StaticCacheHashLength;
/// <summary>
/// Gets or sets a value for the cache folder.
/// Gets or sets a value for the image cache folder depth.
/// </summary>
[DefaultValue(StaticCacheFolderDepth)]
public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth;
/// <summary>
/// Gets or sets a value for the image cache folder.
/// </summary>
[DefaultValue(StaticCacheFolder)]
public string CacheFolder { get; set; } = StaticCacheFolder;

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated.
/// </summary>
public class ImageUrlGenerationOptions
public class ImageUrlGenerationOptions : IEquatable<ImageUrlGenerationOptions>
{
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<FocalPointPosition>.Default.Equals(FocalPoint, other.FocalPoint) &&
EqualityComparer<CropCoordinates>.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();
}
/// <summary>
/// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0.
/// </summary>
public class FocalPointPosition
public class FocalPointPosition : IEquatable<FocalPointPosition>
{
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);
}
/// <summary>
/// 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.
/// </summary>
public class CropCoordinates
public class CropCoordinates : IEquatable<CropCoordinates>
{
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);
}
}
}

View File

@@ -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<IImageDimensionExtractor, ImageSharpDimensionExtractor>();
builder.Services.AddSingleton<IImageUrlGenerator, ImageSharpImageUrlGenerator>();
builder.Services.AddSingleton<PackageDataInstallation>();

View File

@@ -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<ushort> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp.
/// </summary>
/// <seealso cref="Umbraco.Cms.Core.Media.IImageUrlGenerator" />
public class ImageSharpImageUrlGenerator : IImageUrlGenerator
{
/// <inheritdoc />
public IEnumerable<string> SupportedImageFileTypes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
public ImageSharpImageUrlGenerator(Configuration configuration)
: this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
/// </summary>
/// <param name="supportedImageFileTypes">The supported image file types/extensions.</param>
/// <remarks>
/// This constructor is only used for testing.
/// </remarks>
internal ImageSharpImageUrlGenerator(IEnumerable<string> supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes;
/// <inheritdoc/>
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();
}
}
}

View File

@@ -49,7 +49,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="6.0.0" /> <!-- Explicit updated this nested dependency due to this https://github.com/dotnet/announcements/issues/178-->
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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
{
/// <summary>
/// Configures the ImageSharp middleware options.
/// </summary>
/// <seealso cref="IConfigureOptions{ImageSharpMiddlewareOptions}" />
public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions<ImageSharpMiddlewareOptions>
{
private readonly Configuration _configuration;
private readonly ImagingSettings _imagingSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureImageSharpMiddlewareOptions" /> class.
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
/// <param name="imagingSettings">The Umbraco imaging settings.</param>
public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions<ImagingSettings> imagingSettings)
{
_configuration = configuration;
_imagingSettings = imagingSettings.Value;
}
/// <inheritdoc />
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<int>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture);
if (width <= 0 || width > _imagingSettings.Resize.MaxWidth)
{
context.Commands.Remove(ResizeWebProcessor.Width);
}
int height = context.Parser.ParseValue<int>(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;
};
}
}
}

View File

@@ -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
{
/// <summary>
/// Configures the ImageSharp physical file system cache options.
/// </summary>
/// <seealso cref="IConfigureOptions{PhysicalFileSystemCacheOptions}" />
public sealed class ConfigurePhysicalFileSystemCacheOptions : IConfigureOptions<PhysicalFileSystemCacheOptions>
{
private readonly ImagingSettings _imagingSettings;
private readonly IHostEnvironment _hostEnvironment;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurePhysicalFileSystemCacheOptions" /> class.
/// </summary>
/// <param name="imagingSettings">The Umbraco imaging settings.</param>
/// <param name="hostEnvironment">The host environment.</param>
public ConfigurePhysicalFileSystemCacheOptions(IOptions<ImagingSettings> imagingSettings, IHostEnvironment hostEnvironment)
{
_imagingSettings = imagingSettings.Value;
_hostEnvironment = hostEnvironment;
}
/// <inheritdoc />
public void Configure(PhysicalFileSystemCacheOptions options)
{
options.CacheFolder = _hostEnvironment.MapPathContentRoot(_imagingSettings.Cache.CacheFolder);
options.CacheFolderDepth = _imagingSettings.Cache.CacheFolderDepth;
}
}
}

View File

@@ -1,30 +0,0 @@
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Web.Middleware;
namespace Umbraco.Cms.Web.Common.DependencyInjection
{
/// <summary>
/// Configures the ImageSharp middleware options to use the registered configuration.
/// </summary>
/// <seealso cref="IConfigureOptions{ImageSharpMiddlewareOptions}" />
public sealed class ImageSharpConfigurationOptions : IConfigureOptions<ImageSharpMiddlewareOptions>
{
/// <summary>
/// The ImageSharp configuration.
/// </summary>
private readonly Configuration _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="ImageSharpConfigurationOptions" /> class.
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration;
/// <summary>
/// Invoked to configure an <see cref="ImageSharpMiddlewareOptions" /> instance.
/// </summary>
/// <param name="options">The options instance to configure.</param>
public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration;
}
}

View File

@@ -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
/// </summary>
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
{
ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging)
.Get<ImagingSettings>() ?? new ImagingSettings();
builder.Services.AddSingleton<IImageUrlGenerator, ImageSharpImageUrlGenerator>();
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<WebRootImageProvider>()
// Add custom processors
.AddProcessor<CropWebProcessor>();
// 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<IConfigureOptions<ImageSharpMiddlewareOptions>, ConfigureImageSharpMiddlewareOptions>();
uint width = context.Parser.ParseValue<uint>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture);
uint height = context.Parser.ParseValue<uint>(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<CropWebProcessor>();
builder.Services.AddOptions<PhysicalFileSystemCacheOptions>()
.Configure<IHostEnvironment>((opt, hostEnvironment) =>
{
opt.CacheFolder = hostEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder);
});
// Configure middleware to use the registered/shared ImageSharp configuration
builder.Services.AddTransient<IConfigureOptions<ImageSharpMiddlewareOptions>, ImageSharpConfigurationOptions>();
// Configure cache options
builder.Services.AddTransient<IConfigureOptions<PhysicalFileSystemCacheOptions>, ConfigurePhysicalFileSystemCacheOptions>();
return builder.Services;
}

View File

@@ -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
/// </summary>
public const string Coordinates = "cc";
/// <inheritdoc/>
/// <summary>
/// The command constant for the resize orientation handling mode.
/// </summary>
public const string Orient = "orient";
/// <inheritdoc />
public IEnumerable<string> Commands { get; } = new[]
{
Coordinates
Coordinates,
Orient
};
/// <inheritdoc/>
public FormattedImage Process(FormattedImage image, ILogger logger, IDictionary<string, string> commands, CommandParser parser, CultureInfo culture)
/// <inheritdoc />
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<string, string> commands, CommandParser parser, CultureInfo culture)
/// <inheritdoc />
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<float[]>(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<bool>(commands.GetValueOrDefault(Orient), culture))
{
return ExifOrientationMode.Unknown;
}
image.TryGetExifOrientation(out ushort orientation);
return orientation;
}
}
}

View File

@@ -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
{
/// <summary>
/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp.
/// </summary>
/// <seealso cref="IImageUrlGenerator" />
public class ImageSharpImageUrlGenerator : IImageUrlGenerator
{
/// <inheritdoc />
public IEnumerable<string> SupportedImageFileTypes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
public ImageSharpImageUrlGenerator(Configuration configuration)
: this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
/// </summary>
/// <param name="supportedImageFileTypes">The supported image file types/extensions.</param>
/// <remarks>
/// This constructor is only used for testing.
/// </remarks>
internal ImageSharpImageUrlGenerator(IEnumerable<string> supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes;
/// <inheritdoc/>
public string? GetImageUrl(ImageUrlGenerationOptions options)
{
if (options?.ImageUrl == null)
{
return null;
}
var queryString = new Dictionary<string, string?>();
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<string, StringValues> 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);
}
}
}

View File

@@ -1,52 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Library</OutputType>
<RootNamespace>Umbraco.Cms.Web.Common</RootNamespace>
<PackageId>Umbraco.Cms.Web.Common</PackageId>
<Title>Umbraco CMS Web</Title>
<Description>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</Description>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Library</OutputType>
<RootNamespace>Umbraco.Cms.Web.Common</RootNamespace>
<PackageId>Umbraco.Cms.Web.Common</PackageId>
<Title>Umbraco CMS Web</Title>
<Description>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</Description>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\Umbraco.Web.Common.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DocumentationFile>bin\Release\Umbraco.Web.Common.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj" />
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
<ProjectReference Include="..\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj" />
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
<ProjectReference Include="..\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.4" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
<PackageReference Include="NETStandard.Library" Version="2.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="1.0.5" />
<PackageReference Include="Smidge.Nuglify" Version="4.0.4" />
<PackageReference Include="Smidge.InMemory" Version="4.0.4" />
<PackageReference Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageReference Include="Umbraco.Code" Version="2.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.4" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="2.0.0" />
<PackageReference Include="Smidge.InMemory" Version="4.0.4" />
<PackageReference Include="Smidge.Nuglify" Version="4.0.4" />
<PackageReference Include="Umbraco.Code" Version="2.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -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<ICommandConverter>
using var image = new Image<Rgba32>(50, 90);
using var formattedImage = new FormattedImage(image, PngFormat.Instance);
var logger = new NullLogger<ImageSharpMiddleware>();
var commands = new CommandCollection
{
CreateArrayConverterOfFloat(),
CreateSimpleCommandConverterOfFloat(),
{ CropWebProcessor.Coordinates, coordinates },
};
var parser = new CommandParser(converters);
CultureInfo culture = CultureInfo.InvariantCulture;
var commands = new Dictionary<string, string>
var parser = new CommandParser(new ICommandConverter[]
{
{ CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom
};
new ArrayConverter<float>(),
new SimpleCommandConverter<float>()
});
var culture = CultureInfo.InvariantCulture;
using var image = new Image<Rgba32>(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<Rgba32> 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);
}
}
}

View File

@@ -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);
}
/// <summary>
/// Test that if options is null, the generated image URL is also null.
/// </summary>
[Test]
public void GetCropUrlNullTest()
public void GetImageUrlNullOptionsTest()
{
var urlString = s_generator.GetImageUrl(null);
Assert.AreEqual(null, urlString);
}
/// <summary>
/// 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.
/// </summary>
[Test]
public void GetCropUrlEmptyTest()
public void GetImageUrlNullTest()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null));
Assert.AreEqual(null, urlString);
}
/// <summary>
/// Test that if the image URL is empty, the generated image URL is empty.
/// </summary>
[Test]
public void GetImageUrlEmptyTest()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty));
Assert.AreEqual(string.Empty, urlString);
}
/// <summary>
/// Test the GetCropUrl method on the ImageCropDataSet Model
/// Test the GetImageUrl method on the ImageCropDataSet Model
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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);