From f05d5cf837d52b398b25c29cb917c006f0915c53 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 21 Jul 2020 14:41:42 +0200 Subject: [PATCH] Fix image rotation. (#548) --- .../Apps/Json/AppClientsConverter.cs | 1 - .../Assets/AssetCommandMiddleware.cs | 12 +- .../Assets/ImageAssetMetadataSource.cs | 60 ++++++++- .../Assets/AssetFile.cs | 13 +- .../Assets/DelegateAssetFile.cs | 28 ++++ .../Assets/IAssetThumbnailGenerator.cs | 2 + .../Assets/ImageInfo.cs | 6 +- .../ImageSharpAssetThumbnailGenerator.cs | 123 +++++++++++------- backend/src/Squidex.Web/FileExtensions.cs | 2 +- .../Pipeline/RequestExceptionMiddleware.cs | 1 - .../Apps/AppCommandMiddlewareTests.cs | 12 +- .../Apps/AppDomainObjectTests.cs | 6 +- .../Apps/Guards/GuardAppTests.cs | 4 +- .../Assets/AssetCommandMiddlewareTests.cs | 4 +- .../Assets/AssetDomainObjectTests.cs | 3 +- .../Assets/FileTagAssetMetadataSourceTests.cs | 4 +- .../FileTypeAssetMetadataSourceTests.cs | 7 +- .../Assets/ImageAssetMetadataSourceTests.cs | 51 +++++++- .../TestHelpers/NoopAssetFile.cs | 25 ++++ .../ImageSharpAssetThumbnailGeneratorTests.cs | 30 +++++ .../Assets/Images/logo-wide-rotated.jpg | Bin 0 -> 15425 bytes .../Squidex.Infrastructure.Tests.csproj | 2 + .../RequestExceptionMiddlewareTests.cs | 4 - .../TestSuite.ApiTests/AssetFormatTests.cs | 15 +++ .../Assets/logo-wide-rotated.jpg | Bin 0 -> 15425 bytes .../TestSuite.ApiTests.csproj | 6 +- 26 files changed, 320 insertions(+), 101 deletions(-) create mode 100644 backend/src/Squidex.Infrastructure/Assets/DelegateAssetFile.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo-wide-rotated.jpg create mode 100644 backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide-rotated.jpg diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs index 217705ea3..f65e69dcb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Squidex.Infrastructure.Json.Newtonsoft; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 33bac4b6d..bc6d3075d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -54,10 +54,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { case CreateAsset createAsset: { - await EnrichWithHashAndUploadAsync(createAsset, tempFile); - try { + await EnrichWithHashAndUploadAsync(createAsset, tempFile); + var ctx = contextProvider.Context.Clone().WithoutAssetEnrichment(); var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); @@ -80,6 +80,8 @@ namespace Squidex.Domain.Apps.Entities.Assets finally { await assetFileStore.DeleteAsync(tempFile); + + createAsset.File.Dispose(); } break; @@ -87,15 +89,17 @@ namespace Squidex.Domain.Apps.Entities.Assets case UpdateAsset updateAsset: { - await EnrichWithHashAndUploadAsync(updateAsset, tempFile); - try { + await EnrichWithHashAndUploadAsync(updateAsset, tempFile); + await UploadAsync(context, tempFile, updateAsset, null, false, next); } finally { await assetFileStore.DeleteAsync(tempFile); + + updateAsset.File.Dispose(); } break; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index f1e7549b6..fdd1d867f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -5,8 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; +using SixLabors.ImageSharp; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; @@ -25,15 +28,66 @@ namespace Squidex.Domain.Apps.Entities.Assets this.assetThumbnailGenerator = assetThumbnailGenerator; } + private sealed class TempAssetFile : AssetFile, IDisposable + { + public Stream Stream { get; } + + public TempAssetFile(AssetFile source) + : base(source.FileName, source.MimeType, source.FileSize) + { + var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + var tempStream = new FileStream(tempPath, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, 4096, + FileOptions.DeleteOnClose); + + Stream = tempStream; + } + + public override void Dispose() + { + Stream.Dispose(); + } + + public override Stream OpenRead() + { + Stream.Position = 0; + + return Stream; + } + } + public async Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) { - if (command.Type == AssetType.Unknown) + if (command.Type == AssetType.Unknown || command.Type == AssetType.Image) { + ImageInfo? imageInfo = null; + using (var uploadStream = command.File.OpenRead()) { - var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream); + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream); + } + + if (imageInfo != null) + { + var isSwapped = imageInfo.IsRotatedOrSwapped; + + if (isSwapped) + { + var tempFile = new TempAssetFile(command.File); + + using (var uploadStream = command.File.OpenRead()) + { + imageInfo = await assetThumbnailGenerator.FixOrientationAsync(uploadStream, tempFile.Stream); + } + + command.File.Dispose(); + command.File = tempFile; + } - if (imageInfo != null) + if (command.Type == AssetType.Unknown || isSwapped) { command.Type = AssetType.Image; diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs index 4f5ef010f..cf17c74fb 100644 --- a/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs +++ b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs @@ -10,17 +10,15 @@ using System.IO; namespace Squidex.Infrastructure.Assets { - public sealed class AssetFile + public abstract class AssetFile : IDisposable { - private readonly Func openAction; - public string FileName { get; } public string MimeType { get; } public long FileSize { get; } - public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) + protected AssetFile(string fileName, string mimeType, long fileSize) { Guard.NotNullOrEmpty(fileName, nameof(fileName)); Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); @@ -30,13 +28,12 @@ namespace Squidex.Infrastructure.Assets FileSize = fileSize; MimeType = mimeType; - - this.openAction = openAction; } - public Stream OpenRead() + public virtual void Dispose() { - return openAction(); } + + public abstract Stream OpenRead(); } } diff --git a/backend/src/Squidex.Infrastructure/Assets/DelegateAssetFile.cs b/backend/src/Squidex.Infrastructure/Assets/DelegateAssetFile.cs new file mode 100644 index 000000000..b152652f0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/DelegateAssetFile.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class DelegateAssetFile : AssetFile + { + private readonly Func openStream; + + public DelegateAssetFile(string fileName, string mimeType, long fileSize, Func openStream) + : base(fileName, mimeType, fileSize) + { + this.openStream = openStream; + } + + public override Stream OpenRead() + { + return openStream(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs index 4d48f6fe4..b389cedad 100644 --- a/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -14,6 +14,8 @@ namespace Squidex.Infrastructure.Assets { Task GetImageInfoAsync(Stream source); + Task FixOrientationAsync(Stream source, Stream destination); + Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options); } } diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs index 2b3114cf3..fcc8d918d 100644 --- a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs +++ b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -13,13 +13,17 @@ namespace Squidex.Infrastructure.Assets public int PixelHeight { get; } - public ImageInfo(int pixelWidth, int pixelHeight) + public bool IsRotatedOrSwapped { get; } + + public ImageInfo(int pixelWidth, int pixelHeight, bool isRotatedOrSwapped) { Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth)); Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight)); PixelWidth = pixelWidth; PixelHeight = pixelHeight; + + IsRotatedOrSwapped = isRotatedOrSwapped; } } } diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index 5e12251a8..2c44d9340 100644 --- a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -10,6 +10,7 @@ using System.IO; using System.Threading.Tasks; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Processing; using ISResizeMode = SixLabors.ImageSharp.Processing.ResizeMode; using ISResizeOptions = SixLabors.ImageSharp.Processing.ResizeOptions; @@ -22,67 +23,68 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { Guard.NotNull(options, nameof(options)); - return Task.Run(() => + if (!options.IsValid) { - if (!options.IsValid) + source.CopyTo(destination); + + return Task.CompletedTask; + } + + var w = options.Width ?? 0; + var h = options.Height ?? 0; + + using (var image = Image.Load(source, out var format)) + { + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + + if (encoder == null) { - source.CopyTo(destination); + throw new NotSupportedException(); + } - return; + if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat)) + { + encoder = new JpegEncoder { Quality = options.Quality.Value }; } - var w = options.Width ?? 0; - var h = options.Height ?? 0; + image.Mutate(x => x.AutoOrient()); - using (var sourceImage = Image.Load(source, out var format)) + if (w > 0 || h > 0) { - var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + var isCropUpsize = options.Mode == ResizeMode.CropUpsize; - if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat)) + if (!Enum.TryParse(options.Mode.ToString(), true, out var resizeMode)) { - encoder = new JpegEncoder { Quality = options.Quality.Value }; + resizeMode = ISResizeMode.Max; } - if (encoder == null) + if (isCropUpsize) { - throw new NotSupportedException(); + resizeMode = ISResizeMode.Crop; } - if (w > 0 || h > 0) + if (w >= image.Width && h >= image.Height && resizeMode == ISResizeMode.Crop && !isCropUpsize) { - var isCropUpsize = options.Mode == ResizeMode.CropUpsize; - - if (!Enum.TryParse(options.Mode.ToString(), true, out var resizeMode)) - { - resizeMode = ISResizeMode.Max; - } - - if (isCropUpsize) - { - resizeMode = ISResizeMode.Crop; - } - - if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ISResizeMode.Crop && !isCropUpsize) - { - resizeMode = ISResizeMode.BoxPad; - } - - var resizeOptions = new ISResizeOptions { Size = new Size(w, h), Mode = resizeMode }; - - if (options.FocusX.HasValue && options.FocusY.HasValue) - { - resizeOptions.CenterCoordinates = new PointF( - +(options.FocusX.Value / 2f) + 0.5f, - -(options.FocusY.Value / 2f) + 0.5f - ); - } - - sourceImage.Mutate(x => x.Resize(resizeOptions)); + resizeMode = ISResizeMode.BoxPad; } - sourceImage.Save(destination, encoder); + var resizeOptions = new ISResizeOptions { Size = new Size(w, h), Mode = resizeMode }; + + if (options.FocusX.HasValue && options.FocusY.HasValue) + { + resizeOptions.CenterCoordinates = new PointF( + +(options.FocusX.Value / 2f) + 0.5f, + -(options.FocusY.Value / 2f) + 0.5f + ); + } + + image.Mutate(x => x.Resize(resizeOptions)); } - }); + + image.Save(destination, encoder); + } + + return Task.CompletedTask; } public Task GetImageInfoAsync(Stream source) @@ -95,7 +97,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp if (image != null) { - result = new ImageInfo(image.Width, image.Height); + result = GetImageInfo(image); } } catch @@ -105,5 +107,38 @@ namespace Squidex.Infrastructure.Assets.ImageSharp return Task.FromResult(result); } + + public Task FixOrientationAsync(Stream source, Stream destination) + { + using (var image = Image.Load(source, out var format)) + { + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + + if (encoder == null) + { + throw new NotSupportedException(); + } + + image.Mutate(x => x.AutoOrient()); + + image.Save(destination, encoder); + + return Task.FromResult(GetImageInfo(image)); + } + } + + private static ImageInfo GetImageInfo(IImageInfo image) + { + var isRotatedOrSwapped = false; + + if (image.Metadata.ExifProfile != null) + { + var value = image.Metadata.ExifProfile.GetValue(ExifTag.Orientation); + + isRotatedOrSwapped = value?.Value > 1; + } + + return new ImageInfo(image.Width, image.Height, isRotatedOrSwapped); + } } } diff --git a/backend/src/Squidex.Web/FileExtensions.cs b/backend/src/Squidex.Web/FileExtensions.cs index bcd2ad6dc..76e34b849 100644 --- a/backend/src/Squidex.Web/FileExtensions.cs +++ b/backend/src/Squidex.Web/FileExtensions.cs @@ -25,7 +25,7 @@ namespace Squidex.Web throw new ValidationException("File name is not defined."); } - return new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); + return new DelegateAssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); } } } diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs index 3a4abba81..4c2d94b63 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -7,7 +7,6 @@ using System; using System.Threading.Tasks; -using Grpc.Core; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index bc66ea0f8..ac39de1ce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -67,19 +67,17 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task Should_upload_image_to_store() { - var stream = new MemoryStream(); - - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + var file = new NoopAssetFile(); var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); var context = CreateContextForCommand(command); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(new ImageInfo(100, 100)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._)) + .Returns(new ImageInfo(100, 100, false)); await sut.HandleAsync(context); - A.CallTo(() => appImageStore.UploadAsync(appId, stream, A._)) + A.CallTo(() => appImageStore.UploadAsync(appId, A._, A._)) .MustHaveHappened(); } @@ -88,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var stream = new MemoryStream(); - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + var file = new NoopAssetFile(); var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); var context = CreateContextForCommand(command); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index e9d17f017..62a55be89 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; @@ -18,7 +17,6 @@ using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; using Squidex.Shared.Users; @@ -148,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task UploadImage_should_create_events_and_update_image() { - var command = new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => new MemoryStream()) }; + var command = new UploadAppImage { File = new NoopAssetFile() }; await ExecuteCreateAsync(); @@ -691,7 +689,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private Task ExecuteUploadImage() { - return PublishAsync(new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => new MemoryStream()) }); + return PublishAsync(new UploadAppImage { File = new NoopAssetFile() }); } private Task ExecuteAddPatternAsync() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs index 5c0a5b760..8ae626840 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs @@ -5,14 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.IO; using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Validation; using Squidex.Shared.Users; using Xunit; @@ -70,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() { - var command = new UploadAppImage { File = new AssetFile("file.png", "image/png", 100, () => new MemoryStream()) }; + var command = new UploadAppImage { File = new NoopAssetFile() }; GuardApp.CanUploadImage(command); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 97a00be7a..315ae9744 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; using FakeItEasy; @@ -36,7 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IServiceProvider serviceProvider = A.Fake(); private readonly ITagService tagService = A.Fake(); private readonly Guid assetId = Guid.NewGuid(); - private readonly Stream stream = new MemoryStream(); private readonly AssetDomainObjectGrain asset; private readonly AssetFile file; private readonly Context requestContext = Context.Anonymous(); @@ -53,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetCommandMiddlewareTests() { - file = new AssetFile("my-image.png", "image/png", 1024, () => stream); + file = new NoopAssetFile(); var assetDomainObject = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy()); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index 0a7c4fb1f..0f8a5846b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using FakeItEasy; @@ -31,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetQueryService assetQuery = A.Fake(); private readonly Guid parentId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid(); - private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + private readonly AssetFile file = new NoopAssetFile(); private readonly AssetDomainObject sut; protected override Guid Id diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index 30cc6050f..d6592251d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -127,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Assets return new CreateAsset { - File = new AssetFile(file.Name, "mime", file.Length, file.OpenRead) + File = new DelegateAssetFile(file.Name, "mime", file.Length, file.OpenRead) }; } @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Assets return new CreateAsset { - File = new AssetFile(name, "mime", stream.Length, () => stream) + File = new DelegateAssetFile(name, "mime", stream.Length, () => stream) }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs index ea31b9f99..88f4e302f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs @@ -6,10 +6,9 @@ // ========================================================================== using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure.Assets; +using Squidex.Domain.Apps.Entities.TestHelpers; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets @@ -34,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { - File = new AssetFile("File.DOCX", "Mime", 100, () => new MemoryStream()) + File = new NoopAssetFile("File.DOCX") }; await sut.EnhanceAsync(command, tags); @@ -47,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { - File = new AssetFile("File", "Mime", 100, () => new MemoryStream()) + File = new NoopAssetFile("File") }; await sut.EnhanceAsync(command, tags); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 152bced7c..ee87d08c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -27,20 +27,20 @@ namespace Squidex.Domain.Apps.Entities.Assets public ImageAssetMetadataSourceTests() { - file = new AssetFile("MyImage.png", "image/png", 1024, () => stream); + file = new DelegateAssetFile("MyImage.png", "image/png", 1024, () => stream); sut = new ImageAssetMetadataSource(assetThumbnailGenerator); } [Fact] - public async Task Should_not_enhance_if_type_already_found() + public async Task Should_also_enhance_if_type_already_found() { var command = new CreateAsset { File = file, Type = AssetType.Image }; await sut.EnhanceAsync(command, tags); A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._)) - .MustNotHaveHappened(); + .MustHaveHappened(); } [Fact] @@ -53,10 +53,49 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Empty(tags); } + [Fact] + public async Task Should_get_dimensions_from_image_library() + { + var command = new CreateAsset { File = file }; + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(800, 600, false)); + + await sut.EnhanceAsync(command, tags); + + Assert.Equal(800, command.Metadata.GetPixelWidth()); + Assert.Equal(600, command.Metadata.GetPixelHeight()); + Assert.Equal(AssetType.Image, command.Type); + + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_fix_image_if_oriented() + { + var command = new CreateAsset { File = file }; + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(600, 800, true)); + + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, A._)) + .Returns(new ImageInfo(800, 600, true)); + + await sut.EnhanceAsync(command, tags); + + Assert.Equal(800, command.Metadata.GetPixelWidth()); + Assert.Equal(600, command.Metadata.GetPixelHeight()); + Assert.Equal(AssetType.Image, command.Type); + + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, A._)) + .MustHaveHappened(); + } + [Fact] public async Task Should_add_image_tag_if_small() { - var command = new CreateAsset { Type = AssetType.Image }; + var command = new CreateAsset { File = file, Type = AssetType.Image }; command.Metadata.SetPixelWidth(100); command.Metadata.SetPixelWidth(100); @@ -70,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Should_add_image_tag_if_medium() { - var command = new CreateAsset { Type = AssetType.Image }; + var command = new CreateAsset { File = file, Type = AssetType.Image }; command.Metadata.SetPixelWidth(800); command.Metadata.SetPixelWidth(600); @@ -84,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Should_add_image_tag_if_large() { - var command = new CreateAsset { Type = AssetType.Image }; + var command = new CreateAsset { File = file, Type = AssetType.Image }; command.Metadata.SetPixelWidth(1200); command.Metadata.SetPixelWidth(1400); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs new file mode 100644 index 000000000..d26bb381e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public sealed class NoopAssetFile : AssetFile + { + public NoopAssetFile(string fileName = "image.png", string mimeType = "image/png", long fileSize = 1024) + : base(fileName, mimeType, fileSize) + { + } + + public override Stream OpenRead() + { + return new MemoryStream(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs index ce72ed25c..d550e6f25 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -64,6 +64,18 @@ namespace Squidex.Infrastructure.Assets Assert.True(target.Length < source.Length); } + [Fact] + public async Task Should_auto_orient_image() + { + var source = GetRotatedJpeg(); + + var imageInfo = await sut.FixOrientationAsync(source, target); + + 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() { @@ -73,6 +85,19 @@ namespace Squidex.Infrastructure.Assets Assert.Equal(600, imageInfo!.PixelHeight); Assert.Equal(600, imageInfo!.PixelWidth); + Assert.False(imageInfo.IsRotatedOrSwapped); + } + + [Fact] + public async Task Should_return_image_information_if_rotated() + { + var source = GetRotatedJpeg(); + + var imageInfo = await sut.GetImageInfoAsync(source); + + Assert.Equal(600, imageInfo!.PixelHeight); + Assert.Equal(135, imageInfo!.PixelWidth); + Assert.True(imageInfo.IsRotatedOrSwapped); } [Fact] @@ -94,5 +119,10 @@ namespace Squidex.Infrastructure.Assets { return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg")!; } + + private Stream GetRotatedJpeg() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo-wide-rotated.jpg")!; + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo-wide-rotated.jpg b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo-wide-rotated.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0679aba18f01d4fee5a706e7e6f314613aea7dbe GIT binary patch literal 15425 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 literal 0 HcmV?d00001 diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 42be07719..6db6b4dd8 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -7,6 +7,7 @@ enable + @@ -41,6 +42,7 @@ + diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs index 4500ce80d..ed18f50fb 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs @@ -7,15 +7,11 @@ using System; using System.Threading.Tasks; -using Elasticsearch.Net; using FakeItEasy; -using GraphQL; -using Grpc.Core.Logging; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Xunit; diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs index f81db98c4..7e07cc505 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -82,6 +82,21 @@ namespace TestSuite.ApiTests Assert.Equal(AssetType.Image, asset.Type); } + [Fact] + public async Task Should_fix_orientation() + { + var asset = await _.UploadFileAsync("Assets/logo-wide-rotated.jpg", "image/jpg"); + + // Should parse image metadata and fix orientation. + Assert.True(asset.IsImage); + Assert.Equal(135, asset.PixelHeight); + Assert.Equal(600, asset.PixelWidth); + Assert.Equal(135L, asset.Metadata["pixelHeight"]); + Assert.Equal(600L, asset.Metadata["pixelWidth"]); + Assert.Equal(79L, asset.Metadata["imageQuality"]); + Assert.Equal(AssetType.Image, asset.Type); + } + [Fact] public async Task Should_upload_audio_mp3() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide-rotated.jpg b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide-rotated.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0679aba18f01d4fee5a706e7e6f314613aea7dbe GIT binary patch literal 15425 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 literal 0 HcmV?d00001 diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index cf147d566..375a59eaf 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -22,13 +22,13 @@ - - - PreserveNewest + + PreserveNewest + PreserveNewest