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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs
Normal file
100
src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user