diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs index 12e85dfb88..6331ad24e1 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs @@ -70,12 +70,12 @@ namespace Umbraco.Cms.Infrastructure.Media AddQueryString("quality", options.Quality.Value); } - if (!string.IsNullOrWhiteSpace(options.FurtherOptions)) + if (string.IsNullOrWhiteSpace(options.FurtherOptions) == false) { AppendQueryString(options.FurtherOptions.TrimStart('?', '&')); } - if (!string.IsNullOrWhiteSpace(options.CacheBusterValue)) + if (string.IsNullOrWhiteSpace(options.CacheBusterValue) == false) { AddQueryString("rnd", options.CacheBusterValue); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensionsTests.cs new file mode 100644 index 0000000000..0389c51726 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensionsTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Extensions +{ + [TestFixture] + public class ImageCropperTemplateCoreExtensionsTests + { + [Test] + public void GetCropUrl_WithCropSpecifiedButNotFound_ReturnsNull() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + new ImageCropperValue { }, + imageCropMode: ImageCropMode.Crop, + cropAlias: "Missing"); + + Assert.IsNull(result); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndUsingCropDimensions_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + useCropDimensions: true); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 100 && + y.Height == 200))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndWidthAndHeightProvided_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + width: 50, + height: 80); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 50 && + y.Height == 80))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndWidthOnlyProvided_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + width: 50); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 50 && + y.Height == 100))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndHeightOnlyProvided_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + height: 50); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 25 && + y.Height == 50))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndWidthRatioModeProvidedWithHeight_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + ratioMode: ImageCropRatioMode.Width, + height: 50); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 50 && + y.Height == 50))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndWidthRatioModeProvidedWithWidthAndHeight_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + ratioMode: ImageCropRatioMode.Width, + width: 35, + height: 50); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 35 && + y.Height == 50))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndHeightRatioModeProvidedWithWidth_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + ratioMode: ImageCropRatioMode.Height, + width: 60); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 60 && + y.Height == 60))); + } + + [Test] + public void GetCropUrl_WithCropSpecifiedAndHeightRatioModeProvidedWithWidthAndHeight_CallsImageGeneratorWithCorrectParameters() + { + var imageUrl = "/test.jpg"; + Mock imageUrlGenerator = CreateMockImageUrlGenerator(); + var result = imageUrl.GetCropUrl( + imageUrlGenerator.Object, + CreateImageCropperValueWithCrops(), + imageCropMode: ImageCropMode.Crop, + cropAlias: "TestCrop", + ratioMode: ImageCropRatioMode.Height, + width: 60, + height: 40); + + imageUrlGenerator + .Verify(x => x.GetImageUrl( + It.Is(y => y.Width == 60 && + y.Height == 40))); + } + + private static Mock CreateMockImageUrlGenerator() => new Mock(); + + private static ImageCropperValue CreateImageCropperValueWithCrops() => new ImageCropperValue + { + Crops = new List + { + new ImageCropperValue.ImageCropperCrop { Alias = "TestCrop", Width = 100, Height = 200 }, + } + }; + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs new file mode 100644 index 0000000000..2c508d97d2 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using NUnit.Framework; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Commands.Converters; +using Umbraco.Cms.Web.Common.ImageProcessors; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.ImageProcessors +{ + [TestFixture] + public class CropWebProcessorTests + { + [Test] + public void CropWebProcessor_CropsImage() + { + var converters = new List + { + CreateArrayConverterOfFloat(), + CreateSimpleCommandConverterOfFloat(), + }; + + var parser = new CommandParser(converters); + CultureInfo culture = CultureInfo.InvariantCulture; + + var commands = new Dictionary + { + { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom + }; + + using var image = new Image(50, 80); + using FormattedImage formatted = CreateFormattedImage(image, PngFormat.Instance); + new CropWebProcessor().Process(formatted, null, commands, parser, culture); + + Assert.AreEqual(40, image.Width); // Cropped 5 pixels from each side. + Assert.AreEqual(32, image.Height); // Cropped 16 pixels from the top and 32 from the bottom. + } + + private static ICommandConverter CreateArrayConverterOfFloat() + { + // ImageSharp.Web's ArrayConverter is internal, so we need to use reflection to instantiate. + var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.ArrayConverter`1, SixLabors.ImageSharp.Web"); + Type[] typeArgs = { typeof(float) }; + Type genericType = type.MakeGenericType(typeArgs); + return (ICommandConverter)Activator.CreateInstance(genericType); + } + + private static ICommandConverter CreateSimpleCommandConverterOfFloat() + { + // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. + var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); + Type[] typeArgs = { typeof(float) }; + Type genericType = type.MakeGenericType(typeArgs); + return (ICommandConverter)Activator.CreateInstance(genericType); + } + + private FormattedImage CreateFormattedImage(Image image, PngFormat format) + { + // Again, the constructor of FormattedImage useful for tests is internal, so we need to use reflection. + Type type = typeof(FormattedImage); + var instance = type.Assembly.CreateInstance( + type.FullName, + false, + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new object[] { image, format }, + null, + null); + return (FormattedImage)instance; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index ae7e391bba..a6d82e3352 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -324,7 +324,7 @@ namespace Umbraco.Extensions ImageUrlGenerationOptions options; if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - var crop = cropDataSet.GetCrop(cropAlias); + ImageCropperValue.ImageCropperCrop crop = cropDataSet.GetCrop(cropAlias); // If a crop was specified, but not found, return null if (crop == null && !string.IsNullOrWhiteSpace(cropAlias))