diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs index df8678dd9..d4e46c533 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -14,6 +14,6 @@ namespace Squidex.Infrastructure.Assets { Task GetImageInfoAsync(Stream source); - Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode); + Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null); } } diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index f36089eeb..929acb63d 100644 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -9,6 +9,7 @@ using System; using System.IO; using System.Threading.Tasks; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Transforms; using SixLabors.Primitives; @@ -17,49 +18,59 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator { - public ImageSharpAssetThumbnailGenerator() - { - Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Jpeg); - Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Png); - } - - public Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode) + public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null) { return Task.Run(() => { - if (width == null && height == null) + if (!width.HasValue && !height.HasValue && !quality.HasValue) { source.CopyTo(destination); return; } - var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); - - if (!Enum.TryParse(mode, true, out var resizeMode)) - { - resizeMode = ResizeMode.Max; - } - - if (isCropUpsize) + using (var sourceImage = Image.Load(source, out var format)) { - resizeMode = ResizeMode.Crop; - } + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); - var w = width ?? 0; - var h = height ?? 0; + if (quality.HasValue) + { + encoder = new JpegEncoder { Quality = quality.Value }; + } - using (var sourceImage = Image.Load(source, out var format)) - { - if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + if (encoder == null) { - resizeMode = ResizeMode.BoxPad; + throw new NotSupportedException(); } - var options = new ResizeOptions { Size = new Size(w, h), Mode = resizeMode }; + if (width.HasValue || height.HasValue) + { + var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); + + if (!Enum.TryParse(mode, true, out var resizeMode)) + { + resizeMode = ResizeMode.Max; + } + + if (isCropUpsize) + { + resizeMode = ResizeMode.Crop; + } + + var resizeWidth = width ?? 0; + var resizeHeight = height ?? 0; + + if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + { + resizeMode = ResizeMode.BoxPad; + } + + var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; + + sourceImage.Mutate(x => x.Resize(options)); + } - sourceImage.Mutate(x => x.Resize(options)); - sourceImage.Save(destination, format); + sourceImage.Save(destination, encoder); } }); } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index a83185c66..a9afcda05 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -51,6 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The optional version of the asset. /// The target width of the asset, if it is an image. /// The target height of the asset, if it is an image. + /// Optional image quality, it is is an jpeg image. /// The resize mode when the width and height is defined. /// /// 200 => Asset found and content or (resized) image returned. @@ -64,11 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Assets [FromQuery] long version = EtagVersion.Any, [FromQuery] int? width = null, [FromQuery] int? height = null, + [FromQuery] int? quality = null, [FromQuery] string mode = null) { var entity = await assetRepository.FindAssetAsync(id); - if (entity == null || entity.FileVersion < version || width == 0 || height == 0) + if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0) { return NotFound(); } @@ -83,6 +85,11 @@ namespace Squidex.Areas.Api.Controllers.Assets { var assetSuffix = $"{width}_{height}_{mode}"; + if (quality.HasValue) + { + assetSuffix += $"_{quality}"; + } + try { await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream); @@ -103,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Assets using (Profiler.Trace("ResizeImage")) { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode); + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode, quality); destinationStream.Position = 0; } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs index 798707593..f9f75a47d 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -15,16 +15,15 @@ namespace Squidex.Infrastructure.Assets { public class ImageSharpAssetThumbnailGeneratorTests { - private const string Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTM0A1t6AAAADElEQVQYV2P4//8/AAX+Av6nNYGEAAAAAElFTkSuQmCC"; private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); + private readonly MemoryStream target = new MemoryStream(); [Fact] public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() { - var source = new MemoryStream(Convert.FromBase64String(Image)); - var target = new MemoryStream(); + var source = GetPng(); - await sut.CreateThumbnailAsync(source, target, null, null, "resize"); + await sut.CreateThumbnailAsync(source, target); Assert.Equal(target.Length, source.Length); } @@ -32,23 +31,42 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_resize_image_to_target() { - var source = new MemoryStream(Convert.FromBase64String(Image)); - var target = new MemoryStream(); + var source = GetPng(); - await sut.CreateThumbnailAsync(source, target, 100, 100, "resize"); + await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); Assert.True(target.Length > source.Length); } + [Fact] + public async Task Should_change_jpeg_quality_and_write_to_target() + { + var source = GetJpeg(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_change_png_quality_and_write_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + [Fact] public async Task Should_return_image_information_if_image_is_valid() { - var source = new MemoryStream(Convert.FromBase64String(Image)); + var source = GetPng(); var imageInfo = await sut.GetImageInfoAsync(source); - Assert.Equal(1, imageInfo.PixelHeight); - Assert.Equal(1, imageInfo.PixelWidth); + Assert.Equal(600, imageInfo.PixelHeight); + Assert.Equal(600, imageInfo.PixelWidth); } [Fact] @@ -60,5 +78,15 @@ namespace Squidex.Infrastructure.Assets Assert.Null(imageInfo); } + + private Stream GetPng() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png"); + } + + private Stream GetJpeg() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg"); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg new file mode 100644 index 000000000..e5395ad0a Binary files /dev/null and b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg differ diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png new file mode 100644 index 000000000..3cbc19038 Binary files /dev/null and b/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png differ diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 8a596b7aa..2cfddbc5d 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -6,6 +6,10 @@ Squidex.Infrastructure 7.3 + + + + @@ -34,4 +38,8 @@ + + + + \ No newline at end of file