diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageFormat.cs b/backend/src/Squidex.Infrastructure/Assets/ImageFormat.cs new file mode 100644 index 000000000..cf44353ba --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/ImageFormat.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Assets +{ + public enum ImageFormat + { + Auto, + PNG, + JPEG, + TGA, + GIF + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs index fcc8d918d..bf20928dd 100644 --- a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs +++ b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -9,6 +9,8 @@ namespace Squidex.Infrastructure.Assets { public sealed class ImageInfo { + public string? Format { get; set; } + public int PixelWidth { get; } public int PixelHeight { get; } diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index cfcb8e9ca..4c62c5f39 100644 --- a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -10,7 +10,11 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Processing; using ISResizeMode = SixLabors.ImageSharp.Processing.ResizeMode; @@ -44,17 +48,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { using (var image = Image.Load(source, out var format)) { - var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); - - if (encoder == null) - { - throw new NotSupportedException(); - } - - if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat)) - { - encoder = new JpegEncoder { Quality = options.Quality.Value }; - } + var encoder = GetEncoder(options, format); image.Mutate(x => x.AutoOrient()); @@ -99,6 +93,39 @@ namespace Squidex.Infrastructure.Assets.ImageSharp } } + private static IImageEncoder GetEncoder(ResizeOptions options, SixLabors.ImageSharp.Formats.IImageFormat? format) + { + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + + if (encoder == null) + { + throw new NotSupportedException(); + } + + if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat) && options.Format == ImageFormat.Auto) + { + encoder = new JpegEncoder { Quality = options.Quality.Value }; + } + else if (options.Format == ImageFormat.JPEG) + { + encoder = new JpegEncoder(); + } + else if (options.Format == ImageFormat.PNG) + { + encoder = new PngEncoder(); + } + else if (options.Format == ImageFormat.TGA) + { + encoder = new TgaEncoder(); + } + else if (options.Format == ImageFormat.GIF) + { + encoder = new GifEncoder(); + } + + return encoder; + } + public Task GetImageInfoAsync(Stream source) { Guard.NotNull(source, nameof(source)); @@ -107,11 +134,13 @@ namespace Squidex.Infrastructure.Assets.ImageSharp try { - var image = Image.Identify(source); + var image = Image.Identify(source, out var format); if (image != null) { result = GetImageInfo(image); + + result.Format = format.Name; } } catch diff --git a/backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs b/backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs index de5da4393..9a17878b5 100644 --- a/backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs +++ b/backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs @@ -11,6 +11,8 @@ namespace Squidex.Infrastructure.Assets { public sealed class ResizeOptions { + public ImageFormat Format { get; set; } + public ResizeMode Mode { get; set; } public int? Width { get; set; } @@ -27,7 +29,7 @@ namespace Squidex.Infrastructure.Assets public bool IsValid { - get { return Width > 0 || Height > 0 || Quality > 0; } + get { return Width > 0 || Height > 0 || Quality > 0 || Format != ImageFormat.Auto; } } public override string ToString() @@ -58,6 +60,12 @@ namespace Squidex.Infrastructure.Assets sb.Append(FocusY); } + if (Format != ImageFormat.Auto) + { + sb.Append("_format_"); + sb.Append(Format.ToString()); + } + return sb.ToString(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs index 91257506b..5beeb561e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs @@ -87,6 +87,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [FromQuery(Name = "force")] public bool ForceResize { get; set; } + /// + /// True to force a new resize even if it already stored. + /// + [FromQuery(Name = "format")] + public ImageFormat Format { get; set; } + public ResizeOptions ToResizeOptions(IAssetEntity asset) { Guard.NotNull(asset, nameof(asset)); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs index d550e6f25..854f3012c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -6,7 +6,9 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.Assets.ImageSharp; using Xunit; @@ -18,10 +20,43 @@ namespace Squidex.Infrastructure.Assets private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); private readonly MemoryStream target = new MemoryStream(); + public static IEnumerable GetConversions() + { + var allFormats = Enum.GetValues(typeof(ImageFormat)).OfType().Where(x => x != ImageFormat.Auto); + + foreach (var source in allFormats) + { + foreach (var target in allFormats) + { + if (!Equals(target, source)) + { + yield return new object[] { target, source }; + } + } + } + } + + [Theory] + [MemberData(nameof(GetConversions))] + public async Task Should_convert_between_formats(ImageFormat sourceFormat, ImageFormat targetFormat) + { + var source = GetImage(sourceFormat); + + var options = new ResizeOptions { Format = targetFormat }; + + await sut.CreateThumbnailAsync(source, target, options); + + target.Position = 0; + + var imageInfo = await sut.GetImageInfoAsync(target); + + Assert.Equal(targetFormat.ToString(), imageInfo?.Format); + } + [Fact] public async Task Should_return_same_image_if_no_size_and_quality_is_passed_for_thumbnail() { - var source = GetPng(); + var source = GetImage(ImageFormat.PNG); await sut.CreateThumbnailAsync(source, target, new ResizeOptions()); @@ -31,7 +66,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_resize_image_to_target() { - var source = GetPng(); + var source = GetImage(ImageFormat.PNG); var options = new ResizeOptions { Width = 1000, Height = 1000, Mode = ResizeMode.BoxPad }; @@ -43,7 +78,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_change_jpeg_quality_and_write_to_target() { - var source = GetJpeg(); + var source = GetImage(ImageFormat.JPEG); var options = new ResizeOptions { Quality = 10 }; @@ -55,7 +90,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_change_png_quality_and_write_to_target() { - var source = GetPng(); + var source = GetImage(ImageFormat.PNG); var options = new ResizeOptions { Quality = 10 }; @@ -73,18 +108,20 @@ namespace Squidex.Infrastructure.Assets Assert.Equal(135, imageInfo.PixelHeight); Assert.Equal(600, imageInfo.PixelWidth); + Assert.False(imageInfo.IsRotatedOrSwapped); } [Fact] public async Task Should_return_image_information_if_image_is_valid() { - var source = GetPng(); + var source = GetImage(ImageFormat.PNG); var imageInfo = await sut.GetImageInfoAsync(source); Assert.Equal(600, imageInfo!.PixelHeight); Assert.Equal(600, imageInfo!.PixelWidth); + Assert.False(imageInfo.IsRotatedOrSwapped); } @@ -97,6 +134,7 @@ namespace Squidex.Infrastructure.Assets Assert.Equal(600, imageInfo!.PixelHeight); Assert.Equal(135, imageInfo!.PixelWidth); + Assert.True(imageInfo.IsRotatedOrSwapped); } @@ -110,19 +148,18 @@ namespace Squidex.Infrastructure.Assets Assert.Null(imageInfo); } - private Stream GetPng() + private Stream GetImage(ImageFormat format) { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png")!; - } + var name = $"Squidex.Infrastructure.Assets.Images.logo.{format.ToString().ToLowerInvariant()}"; - private Stream GetJpeg() - { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg")!; + return GetType().Assembly.GetManifestResourceStream(name)!; } private Stream GetRotatedJpeg() { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo-wide-rotated.jpg")!; + var name = "Squidex.Infrastructure.Assets.Images.logo-wide-rotated.jpg"; + + return GetType().Assembly.GetManifestResourceStream(name)!; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.gif b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.gif new file mode 100644 index 000000000..b870aec4c Binary files /dev/null and b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.gif differ diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpeg similarity index 100% rename from backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg rename to backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpeg diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.tga b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.tga new file mode 100644 index 000000000..8f74285fe Binary files /dev/null and b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.tga differ diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 0a5e2ef1b..c26e901fe 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -8,8 +8,10 @@ - + + + @@ -47,8 +49,10 @@ - + + +