Skip to content

Commit 831efea

Browse files
authored
Merge pull request #10623 from umbraco/v9/bugfix/image-cropping
V9: Fix ImageSharp integration and add support for custom crops
2 parents 5d5ee57 + 0b53249 commit 831efea

File tree

27 files changed

+791
-822
lines changed

27 files changed

+791
-822
lines changed
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1-
using System.Collections.Generic;
1+
using System.Collections.Generic;
22
using Umbraco.Cms.Core.Models;
33

44
namespace Umbraco.Cms.Core.Media
55
{
6+
/// <summary>
7+
/// Exposes a method that generates an image URL based on the specified options.
8+
/// </summary>
69
public interface IImageUrlGenerator
710
{
11+
/// <summary>
12+
/// Gets the supported image file types/extensions.
13+
/// </summary>
14+
/// <value>
15+
/// The supported image file types/extensions.
16+
/// </value>
817
IEnumerable<string> SupportedImageFileTypes { get; }
918

19+
/// <summary>
20+
/// Gets the image URL based on the specified <paramref name="options" />.
21+
/// </summary>
22+
/// <param name="options">The image URL generation options.</param>
23+
/// <returns>
24+
/// The generated image URL.
25+
/// </returns>
1026
string GetImageUrl(ImageUrlGenerationOptions options);
1127
}
1228
}

src/Umbraco.Core/Models/ImageCropRatioMode.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,68 @@
1-
namespace Umbraco.Cms.Core.Models
1+
namespace Umbraco.Cms.Core.Models
22
{
33
/// <summary>
4-
/// These are options that are passed to the IImageUrlGenerator implementation to determine
5-
/// the propery URL that is needed
4+
/// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated.
65
/// </summary>
76
public class ImageUrlGenerationOptions
87
{
9-
public ImageUrlGenerationOptions (string imageUrl)
10-
{
11-
ImageUrl = imageUrl;
12-
}
8+
public ImageUrlGenerationOptions(string imageUrl) => ImageUrl = imageUrl;
139

1410
public string ImageUrl { get; }
11+
1512
public int? Width { get; set; }
13+
1614
public int? Height { get; set; }
17-
public decimal? WidthRatio { get; set; }
18-
public decimal? HeightRatio { get; set; }
15+
1916
public int? Quality { get; set; }
17+
2018
public ImageCropMode? ImageCropMode { get; set; }
19+
2120
public ImageCropAnchor? ImageCropAnchor { get; set; }
22-
public bool DefaultCrop { get; set; }
21+
2322
public FocalPointPosition FocalPoint { get; set; }
23+
2424
public CropCoordinates Crop { get; set; }
25+
2526
public string CacheBusterValue { get; set; }
27+
2628
public string FurtherOptions { get; set; }
27-
public bool UpScale { get; set; } = true;
28-
public string AnimationProcessMode { get; set; }
2929

3030
/// <summary>
31-
/// The focal point position, in whatever units the registered IImageUrlGenerator uses,
32-
/// typically a percentage of the total image from 0.0 to 1.0.
31+
/// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0.
3332
/// </summary>
3433
public class FocalPointPosition
3534
{
36-
public FocalPointPosition (decimal top, decimal left)
35+
public FocalPointPosition(decimal left, decimal top)
3736
{
3837
Left = left;
3938
Top = top;
4039
}
4140

4241
public decimal Left { get; }
42+
4343
public decimal Top { get; }
4444
}
4545

4646
/// <summary>
47-
/// The bounds of the crop within the original image, in whatever units the registered
48-
/// IImageUrlGenerator uses, typically a percentage between 0 and 100.
47+
/// 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.
4948
/// </summary>
5049
public class CropCoordinates
5150
{
52-
public CropCoordinates (decimal x1, decimal y1, decimal x2, decimal y2)
51+
public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom)
5352
{
54-
X1 = x1;
55-
Y1 = y1;
56-
X2 = x2;
57-
Y2 = y2;
53+
Left = left;
54+
Top = top;
55+
Right = right;
56+
Bottom = bottom;
5857
}
5958

60-
public decimal X1 { get; }
61-
public decimal Y1 { get; }
62-
public decimal X2 { get; }
63-
public decimal Y2 { get; }
59+
public decimal Left { get; }
60+
61+
public decimal Top { get; }
62+
63+
public decimal Right { get; }
64+
65+
public decimal Bottom { get; }
6466
}
6567
}
6668
}

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
144144
builder.PropertyValueConverters()
145145
.Remove<SimpleTinyMceValueConverter>();
146146

147-
builder.Services.AddUnique<IImageUrlGenerator, ImageSharpImageUrlGenerator>();
148-
149147
// register *all* checks, except those marked [HideFromTypeFinder] of course
150148
builder.Services.AddUnique<IMarkdownToHtmlConverter, MarkdownToHtmlConverter>();
151149

@@ -180,7 +178,10 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
180178

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

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

185186
builder.Services.AddUnique<PackageDataInstallation>();
186187

src/Umbraco.Infrastructure/Media/ImageDimensionExtractor.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ namespace Umbraco.Cms.Infrastructure.Media
88
{
99
internal class ImageDimensionExtractor : IImageDimensionExtractor
1010
{
11+
/// <summary>
12+
/// The ImageSharp configuration.
13+
/// </summary>
14+
private readonly Configuration _configuration;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ImageDimensionExtractor" /> class.
18+
/// </summary>
19+
/// <param name="configuration">The ImageSharp configuration.</param>
20+
public ImageDimensionExtractor(Configuration configuration) => _configuration = configuration;
21+
1122
/// <summary>
1223
/// Gets the dimensions of an image.
1324
/// </summary>
@@ -39,7 +50,7 @@ public ImageSize GetDimensions(Stream stream)
3950
stream.Seek(0, SeekOrigin.Begin);
4051
}
4152

42-
using (var image = Image.Load(stream))
53+
using (var image = Image.Load(_configuration, stream))
4354
{
4455
var fileWidth = image.Width;
4556
var fileHeight = image.Height;
Lines changed: 81 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,107 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Globalization;
4+
using System.Linq;
35
using System.Text;
6+
using SixLabors.ImageSharp;
47
using Umbraco.Cms.Core.Media;
58
using Umbraco.Cms.Core.Models;
6-
using Umbraco.Extensions;
79

810
namespace Umbraco.Cms.Infrastructure.Media
911
{
12+
/// <summary>
13+
/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp.
14+
/// </summary>
15+
/// <seealso cref="Umbraco.Cms.Core.Media.IImageUrlGenerator" />
1016
public class ImageSharpImageUrlGenerator : IImageUrlGenerator
1117
{
12-
public IEnumerable<string> SupportedImageFileTypes => new[] { "jpeg", "jpg", "gif", "bmp", "png" };
18+
/// <inheritdoc />
19+
public IEnumerable<string> SupportedImageFileTypes { get; }
1320

21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
23+
/// </summary>
24+
/// <param name="configuration">The ImageSharp configuration.</param>
25+
public ImageSharpImageUrlGenerator(Configuration configuration)
26+
: this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray())
27+
{ }
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
31+
/// </summary>
32+
/// <param name="supportedImageFileTypes">The supported image file types/extensions.</param>
33+
/// <remarks>
34+
/// This constructor is only used for testing.
35+
/// </remarks>
36+
internal ImageSharpImageUrlGenerator(IEnumerable<string> supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes;
37+
38+
/// <inheritdoc/>
1439
public string GetImageUrl(ImageUrlGenerationOptions options)
1540
{
16-
if (options == null) return null;
41+
if (options == null)
42+
{
43+
return null;
44+
}
1745

18-
var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty);
46+
var imageUrl = new StringBuilder(options.ImageUrl);
1947

20-
if (options.FocalPoint != null) AppendFocalPoint(imageProcessorUrl, options);
21-
else if (options.Crop != null) AppendCrop(imageProcessorUrl, options);
22-
else if (options.DefaultCrop) imageProcessorUrl.Append("?anchor=center&mode=crop");
23-
else
48+
bool queryStringHasStarted = false;
49+
void AppendQueryString(string value)
2450
{
25-
imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? ImageCropMode.Crop).ToString().ToLower());
51+
imageUrl.Append(queryStringHasStarted ? '&' : '?');
52+
queryStringHasStarted = true;
2653

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

30-
var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format=");
59+
if (options.FocalPoint != null)
60+
{
61+
AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top);
62+
}
3163

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

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

47-
return imageProcessorUrl.ToString();
48-
}
74+
if (options.ImageCropAnchor.HasValue)
75+
{
76+
AddQueryString("ranchor", options.ImageCropAnchor.Value.ToString().ToLowerInvariant());
77+
}
4978

50-
private void AppendFocalPoint(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options)
51-
{
52-
imageProcessorUrl.Append("?center=");
53-
imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(",");
54-
imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture));
55-
imageProcessorUrl.Append("&mode=crop");
56-
}
79+
if (options.Width.HasValue)
80+
{
81+
AddQueryString("width", options.Width.Value);
82+
}
5783

58-
private void AppendCrop(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options)
59-
{
60-
imageProcessorUrl.Append("?crop=");
61-
imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(",");
62-
imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(",");
63-
imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(",");
64-
imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture));
65-
imageProcessorUrl.Append("&cropmode=percentage");
84+
if (options.Height.HasValue)
85+
{
86+
AddQueryString("height", options.Height.Value);
87+
}
88+
89+
if (options.Quality.HasValue)
90+
{
91+
AddQueryString("quality", options.Quality.Value);
92+
}
93+
94+
if (string.IsNullOrWhiteSpace(options.FurtherOptions) == false)
95+
{
96+
AppendQueryString(options.FurtherOptions.TrimStart('?', '&'));
97+
}
98+
99+
if (string.IsNullOrWhiteSpace(options.CacheBusterValue) == false)
100+
{
101+
AddQueryString("rnd", options.CacheBusterValue);
102+
}
103+
104+
return imageUrl.ToString();
66105
}
67106
}
68107
}

src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Umbraco.
1+
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

44
using System;
@@ -63,21 +63,19 @@ public ImageCropperCrop GetCrop(string alias)
6363
: Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
6464
}
6565

66-
public ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint)
66+
public ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool preferFocalPoint)
6767
{
68-
if (preferFocalPoint && HasFocalPoint()
69-
|| crop != null && crop.Coordinates == null && HasFocalPoint()
70-
|| defaultCrop && HasFocalPoint())
68+
if ((preferFocalPoint && HasFocalPoint()) || (crop != null && crop.Coordinates == null && HasFocalPoint()))
7169
{
72-
return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Top, FocalPoint.Left) };
70+
return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Left, FocalPoint.Top) };
7371
}
7472
else if (crop != null && crop.Coordinates != null && preferFocalPoint == false)
7573
{
7674
return new ImageUrlGenerationOptions(url) { Crop = new ImageUrlGenerationOptions.CropCoordinates(crop.Coordinates.X1, crop.Coordinates.Y1, crop.Coordinates.X2, crop.Coordinates.Y2) };
7775
}
7876
else
7977
{
80-
return new ImageUrlGenerationOptions(url) { DefaultCrop = true };
78+
return new ImageUrlGenerationOptions(url);
8179
}
8280
}
8381

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

95-
var options = GetCropBaseOptions(string.Empty, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint);
93+
var options = GetCropBaseOptions(null, crop, useFocalPoint || string.IsNullOrWhiteSpace(alias));
9694

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

115113
options.Width = width;
116114
options.Height = height;

0 commit comments

Comments
 (0)