Skip to content

V9: Fix ImageSharp integration and add support for custom crops #10623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a8dd90d
Add CropWebProcessor
nikolajlauridsen Jul 8, 2021
865a5b9
Add ParseDecimal method
nikolajlauridsen Jul 8, 2021
753f6f6
Clean url generator a bit
nikolajlauridsen Jul 8, 2021
d7795cf
Fix unit tests
nikolajlauridsen Jul 8, 2021
6376a32
Use CommandParser to parse commands
nikolajlauridsen Jul 9, 2021
ba2e4cd
Use decimal instead of float for coordinates
nikolajlauridsen Jul 9, 2021
1814b70
Only remove the resize processor
nikolajlauridsen Jul 9, 2021
cacef54
Round to int instead of just converting
nikolajlauridsen Jul 9, 2021
c93fe0d
Add method descriptions
nikolajlauridsen Jul 9, 2021
c255729
Change back to ClearProcessors
nikolajlauridsen Jul 9, 2021
f4ac0a5
Merge branch 'v9/dev' into v9/bugfix/image-cropping
ronaldbarendse Aug 6, 2021
ca5af1e
Only add our custom CropWebProcessor to ImageSharp
ronaldbarendse Aug 6, 2021
20007db
Add cropmode support and align logic to ImageProcessors Crop processor
ronaldbarendse Aug 6, 2021
760c15e
Add cropmode paramerter back to tests
ronaldbarendse Aug 6, 2021
4b85edd
Update ImageSharpImageUrlGenerator
ronaldbarendse Aug 6, 2021
6430b36
Update resize mode and anchor query string parameters
ronaldbarendse Aug 6, 2021
fd93248
Move CropMode enum into seperate file
ronaldbarendse Aug 9, 2021
5cdcd02
Remove support for whole percentage crop values
ronaldbarendse Aug 9, 2021
c21656a
Change processor order and fix crop percentage calculation
ronaldbarendse Aug 9, 2021
50d8e74
Obsolete unsupported image URL generation options and update ImageSha…
ronaldbarendse Aug 9, 2021
0b9f1f4
Remove usage of obsolete image URL generation properties (width/heigh…
ronaldbarendse Aug 10, 2021
474b63a
Update AngularJS image URL parameters
ronaldbarendse Aug 10, 2021
0753353
Remove obsolete/unused properties and parameters
ronaldbarendse Aug 10, 2021
766530f
Swap focal point order to left,top to match resize X,Y coordinates
ronaldbarendse Aug 10, 2021
2281403
Update FocalPointPosition and CropCoordinates constructors/properties
ronaldbarendse Aug 10, 2021
01559ed
Only support crop coordinates in CropWebProcessor
ronaldbarendse Aug 10, 2021
c5361ec
Improve crop coordinate calculations
ronaldbarendse Aug 11, 2021
902984f
Added unit tests for CropWebProcessor and ImageCropperTemplateCoreExt…
AndyButland Aug 11, 2021
8b1bd14
Remove ImageCropRatioMode and clean up GetCropUrl overloads
ronaldbarendse Aug 12, 2021
8ca35f5
Removed two effectively duplicate tests.
AndyButland Aug 12, 2021
74ea311
Remove width/height when both are above the configured maximums, add …
ronaldbarendse Aug 13, 2021
36568a0
Move ImageSharpImageUrlGenerator to Web.Common and add NoopImageUrlGe…
ronaldbarendse Aug 13, 2021
a0184d5
Remove repeated ImageSharp default configuration
ronaldbarendse Aug 13, 2021
42c6aa8
Register default ImageSharp configuration for application-wide use
ronaldbarendse Aug 16, 2021
992eb58
Move ImageSharpImageUrlGenerator back to Infrastructure and remove No…
ronaldbarendse Aug 16, 2021
43704da
Remove dimensions if either one is above the configured maximum
ronaldbarendse Aug 16, 2021
0b53249
Make ImageSharpConfigurationOptions public
ronaldbarendse Aug 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/Umbraco.Core/Media/IImageUrlGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;

namespace Umbraco.Cms.Core.Media
{
/// <summary>
/// Exposes a method that generates an image URL based on the specified options.
/// </summary>
public interface IImageUrlGenerator
{
/// <summary>
/// Gets the supported image file types/extensions.
/// </summary>
/// <value>
/// The supported image file types/extensions.
/// </value>
IEnumerable<string> SupportedImageFileTypes { get; }

/// <summary>
/// Gets the image URL based on the specified <paramref name="options" />.
/// </summary>
/// <param name="options">The image URL generation options.</param>
/// <returns>
/// The generated image URL.
/// </returns>
string GetImageUrl(ImageUrlGenerationOptions options);
}
}
8 changes: 0 additions & 8 deletions src/Umbraco.Core/Models/ImageCropRatioMode.cs

This file was deleted.

54 changes: 28 additions & 26 deletions src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs
Original file line number Diff line number Diff line change
@@ -1,66 +1,68 @@
namespace Umbraco.Cms.Core.Models
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// These are options that are passed to the IImageUrlGenerator implementation to determine
/// the propery URL that is needed
/// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated.
/// </summary>
public class ImageUrlGenerationOptions
{
public ImageUrlGenerationOptions (string imageUrl)
{
ImageUrl = imageUrl;
}
public ImageUrlGenerationOptions(string imageUrl) => ImageUrl = imageUrl;

public string ImageUrl { get; }

public int? Width { get; set; }

public int? Height { get; set; }
public decimal? WidthRatio { get; set; }
public decimal? HeightRatio { get; set; }

public int? Quality { get; set; }

public ImageCropMode? ImageCropMode { get; set; }

public ImageCropAnchor? ImageCropAnchor { get; set; }
public bool DefaultCrop { get; set; }

public FocalPointPosition FocalPoint { get; set; }

public CropCoordinates Crop { get; set; }

public string CacheBusterValue { get; set; }

public string FurtherOptions { get; set; }
public bool UpScale { get; set; } = true;
public string AnimationProcessMode { get; set; }

/// <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.
/// 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 FocalPointPosition (decimal top, decimal left)
public FocalPointPosition(decimal left, decimal top)
{
Left = left;
Top = top;
}

public decimal Left { get; }

public decimal Top { get; }
}

/// <summary>
/// The bounds of the crop within the original image, in whatever units the registered
/// IImageUrlGenerator uses, typically a percentage between 0 and 100.
/// 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 CropCoordinates (decimal x1, decimal y1, decimal x2, decimal y2)
public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom)
{
X1 = x1;
Y1 = y1;
X2 = x2;
Y2 = y2;
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}

public decimal X1 { get; }
public decimal Y1 { get; }
public decimal X2 { get; }
public decimal Y2 { get; }
public decimal Left { get; }

public decimal Top { get; }

public decimal Right { get; }

public decimal Bottom { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,6 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
builder.PropertyValueConverters()
.Remove<SimpleTinyMceValueConverter>();

builder.Services.AddUnique<IImageUrlGenerator, ImageSharpImageUrlGenerator>();

// register *all* checks, except those marked [HideFromTypeFinder] of course
builder.Services.AddUnique<IMarkdownToHtmlConverter, MarkdownToHtmlConverter>();

Expand Down Expand Up @@ -180,7 +178,10 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde

builder.Services.AddUnique<ICronTabParser, NCronTabParser>();

// Add default ImageSharp configuration and service implementations
builder.Services.AddUnique(SixLabors.ImageSharp.Configuration.Default);
builder.Services.AddUnique<IImageDimensionExtractor, ImageDimensionExtractor>();
builder.Services.AddUnique<IImageUrlGenerator, ImageSharpImageUrlGenerator>();

builder.Services.AddUnique<PackageDataInstallation>();

Expand Down
13 changes: 12 additions & 1 deletion src/Umbraco.Infrastructure/Media/ImageDimensionExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ namespace Umbraco.Cms.Infrastructure.Media
{
internal class ImageDimensionExtractor : IImageDimensionExtractor
{
/// <summary>
/// The ImageSharp configuration.
/// </summary>
private readonly Configuration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="ImageDimensionExtractor" /> class.
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
public ImageDimensionExtractor(Configuration configuration) => _configuration = configuration;

/// <summary>
/// Gets the dimensions of an image.
/// </summary>
Expand Down Expand Up @@ -39,7 +50,7 @@ public ImageSize GetDimensions(Stream stream)
stream.Seek(0, SeekOrigin.Begin);
}

using (var image = Image.Load(stream))
using (var image = Image.Load(_configuration, stream))
{
var fileWidth = image.Width;
var fileHeight = image.Height;
Expand Down
123 changes: 81 additions & 42 deletions src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,68 +1,107 @@
using System.Collections.Generic;
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;
using Umbraco.Extensions;

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
{
public IEnumerable<string> SupportedImageFileTypes => new[] { "jpeg", "jpg", "gif", "bmp", "png" };
/// <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;
if (options == null)
{
return null;
}

var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty);
var imageUrl = new StringBuilder(options.ImageUrl);

if (options.FocalPoint != null) AppendFocalPoint(imageProcessorUrl, options);
else if (options.Crop != null) AppendCrop(imageProcessorUrl, options);
else if (options.DefaultCrop) imageProcessorUrl.Append("?anchor=center&mode=crop");
else
bool queryStringHasStarted = false;
void AppendQueryString(string value)
{
imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? ImageCropMode.Crop).ToString().ToLower());
imageUrl.Append(queryStringHasStarted ? '&' : '?');
queryStringHasStarted = true;

if (options.ImageCropAnchor != null) imageProcessorUrl.Append("&anchor=").Append(options.ImageCropAnchor.ToString().ToLower());
imageUrl.Append(value);
}
void AddQueryString(string key, params IConvertible[] values)
=> AppendQueryString(key + '=' + string.Join(",", values.Select(x => x.ToString(CultureInfo.InvariantCulture))));

var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format=");
if (options.FocalPoint != null)
{
AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top);
}

//Only put quality here, if we don't have a format specified.
//Otherwise we need to put quality at the end to avoid it being overridden by the format.
if (options.Quality.HasValue && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality);
if (options.HeightRatio.HasValue) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture));
if (options.WidthRatio.HasValue) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture));
if (options.Width.HasValue) imageProcessorUrl.Append("&width=").Append(options.Width);
if (options.Height.HasValue) imageProcessorUrl.Append("&height=").Append(options.Height);
if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false");
if (!string.IsNullOrWhiteSpace(options.AnimationProcessMode)) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode);
if (!string.IsNullOrWhiteSpace(options.FurtherOptions)) imageProcessorUrl.Append(options.FurtherOptions);
if (options.Crop != null)
{
AddQueryString("cc", options.Crop.Left, options.Crop.Top, options.Crop.Right, options.Crop.Bottom);
}

//If furtherOptions contains a format, we need to put the quality after the format.
if (options.Quality.HasValue && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality);
if (!string.IsNullOrWhiteSpace(options.CacheBusterValue)) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue);
if (options.ImageCropMode.HasValue)
{
AddQueryString("rmode", options.ImageCropMode.Value.ToString().ToLowerInvariant());
}

return imageProcessorUrl.ToString();
}
if (options.ImageCropAnchor.HasValue)
{
AddQueryString("ranchor", options.ImageCropAnchor.Value.ToString().ToLowerInvariant());
}

private void AppendFocalPoint(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options)
{
imageProcessorUrl.Append("?center=");
imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(",");
imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture));
imageProcessorUrl.Append("&mode=crop");
}
if (options.Width.HasValue)
{
AddQueryString("width", options.Width.Value);
}

private void AppendCrop(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options)
{
imageProcessorUrl.Append("?crop=");
imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(",");
imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(",");
imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(",");
imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture));
imageProcessorUrl.Append("&cropmode=percentage");
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.

using System;
Expand Down Expand Up @@ -63,21 +63,19 @@ public ImageCropperCrop GetCrop(string alias)
: Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}

public ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint)
public ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool preferFocalPoint)
{
if (preferFocalPoint && HasFocalPoint()
|| crop != null && crop.Coordinates == null && HasFocalPoint()
|| defaultCrop && HasFocalPoint())
if ((preferFocalPoint && HasFocalPoint()) || (crop != null && crop.Coordinates == null && HasFocalPoint()))
{
return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Top, FocalPoint.Left) };
return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Left, FocalPoint.Top) };
}
else if (crop != null && crop.Coordinates != null && preferFocalPoint == false)
{
return new ImageUrlGenerationOptions(url) { Crop = new ImageUrlGenerationOptions.CropCoordinates(crop.Coordinates.X1, crop.Coordinates.Y1, crop.Coordinates.X2, crop.Coordinates.Y2) };
}
else
{
return new ImageUrlGenerationOptions(url) { DefaultCrop = true };
return new ImageUrlGenerationOptions(url);
}
}

Expand All @@ -92,7 +90,7 @@ public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, boo
if (crop == null && !string.IsNullOrWhiteSpace(alias))
return null;

var options = GetCropBaseOptions(string.Empty, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint);
var options = GetCropBaseOptions(null, crop, useFocalPoint || string.IsNullOrWhiteSpace(alias));

if (crop != null && useCropDimensions)
{
Expand All @@ -108,9 +106,9 @@ public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, boo
/// <summary>
/// Gets the value image URL for a specific width and height.
/// </summary>
public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, bool useFocalPoint = false, string cacheBusterValue = null)
public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue = null)
{
var options = GetCropBaseOptions(string.Empty, null, true, useFocalPoint);
var options = GetCropBaseOptions(null, null, false);

options.Width = width;
options.Height = height;
Expand Down
Loading