Browse Source

Fix image rotation. (#548)

pull/553/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
f05d5cf837
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs
  2. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  3. 60
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  4. 13
      backend/src/Squidex.Infrastructure/Assets/AssetFile.cs
  5. 28
      backend/src/Squidex.Infrastructure/Assets/DelegateAssetFile.cs
  6. 2
      backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs
  7. 6
      backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs
  8. 123
      backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  9. 2
      backend/src/Squidex.Web/FileExtensions.cs
  10. 1
      backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs
  11. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs
  12. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  13. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs
  14. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  15. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  16. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs
  17. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs
  18. 51
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  19. 25
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs
  20. 30
      backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs
  21. BIN
      backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo-wide-rotated.jpg
  22. 2
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  23. 4
      backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs
  24. 15
      backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
  25. BIN
      backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide-rotated.jpg
  26. 6
      backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

1
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;

12
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;

60
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<string>? 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;

13
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<Stream> openAction;
public string FileName { get; }
public string MimeType { get; }
public long FileSize { get; }
public AssetFile(string fileName, string mimeType, long fileSize, Func<Stream> 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();
}
}

28
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<Stream> openStream;
public DelegateAssetFile(string fileName, string mimeType, long fileSize, Func<Stream> openStream)
: base(fileName, mimeType, fileSize)
{
this.openStream = openStream;
}
public override Stream OpenRead()
{
return openStream();
}
}
}

2
backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs

@ -14,6 +14,8 @@ namespace Squidex.Infrastructure.Assets
{
Task<ImageInfo?> GetImageInfoAsync(Stream source);
Task<ImageInfo> FixOrientationAsync(Stream source, Stream destination);
Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options);
}
}

6
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;
}
}
}

123
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<ISResizeMode>(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<ISResizeMode>(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<ImageInfo?> 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<ImageInfo> 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);
}
}
}

2
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);
}
}
}

1
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;

12
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<Stream>._))
.Returns(new ImageInfo(100, 100, false));
await sut.HandleAsync(context);
A.CallTo(() => appImageStore.UploadAsync(appId, stream, A<CancellationToken>._))
A.CallTo(() => appImageStore.UploadAsync(appId, A<Stream>._, A<CancellationToken>._))
.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);

6
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()

4
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);
}

4
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<IServiceProvider>();
private readonly ITagService tagService = A.Fake<ITagService>();
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<ISemanticLog>());

3
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<IAssetQueryService>();
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

4
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)
};
}
}

7
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);

51
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<Stream>._))
.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<Stream>._))
.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<Stream>._))
.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<Stream>._))
.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);

25
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();
}
}
}

30
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")!;
}
}
}

BIN
backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo-wide-rotated.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -7,6 +7,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Images\logo-wide-rotated.jpg" />
<None Remove="Assets\Images\logo.jpg" />
<None Remove="Assets\Images\logo.png" />
</ItemGroup>
@ -41,6 +42,7 @@
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Images\logo-wide-rotated.jpg" />
<EmbeddedResource Include="Assets\Images\logo.jpg" />
<EmbeddedResource Include="Assets\Images\logo.png" />
</ItemGroup>

4
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;

15
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()
{

BIN
backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide-rotated.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

6
backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -22,13 +22,13 @@
<ItemGroup>
<ProjectReference Include="..\TestSuite.Shared\TestSuite.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\logo-squared.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\logo-wide-rotated.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\logo-wide.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Loading…
Cancel
Save