From 5ac5743866a7dbd596cf74ae2d9d9948e8c67627 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 19 Oct 2023 15:57:51 +0800 Subject: [PATCH 1/2] Add `Volo.Abp.Imaging.SkiaSharp` package. Resolve #17909 --- Directory.Packages.props | 1 + framework/Volo.Abp.sln | 14 +++ .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 ++++++ .../Volo.Abp.Imaging.SkiaSharp.csproj | 26 +++++ .../Abp/Imaging/AbpImagingSkiaSharpModule.cs | 8 ++ .../SkiaSharpImageResizerContributor.cs | 98 +++++++++++++++++++ .../Abp/Imaging/SkiaSharpResizerOptions.cs | 16 +++ .../Volo.Abp.Imaging.SkiaSharp.Tests.csproj | 19 ++++ .../Imaging/AbpImagingMagickNetTestModule.cs | 14 +++ .../Imaging/AbpImagingSkiaSharpTestModule.cs | 11 +++ .../Abp/Imaging/SkiaSharpImageResizerTests.cs | 75 ++++++++++++++ nupkg/common.ps1 | 1 + 13 files changed, 316 insertions(+) create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xml create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xsd create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/AbpImagingSkiaSharpModule.cs create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs create mode 100644 framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpResizerOptions.cs create mode 100644 framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo.Abp.Imaging.SkiaSharp.Tests.csproj create mode 100644 framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingMagickNetTestModule.cs create mode 100644 framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingSkiaSharpTestModule.cs create mode 100644 framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ae21b20429..e22ec5b093 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -140,6 +140,7 @@ + diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index cd003b7f07..42339a60ac 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -459,6 +459,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Imaging.AspNetCore EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Maui.Client", "src\Volo.Abp.Maui.Client\Volo.Abp.Maui.Client.csproj", "{F19A6E0C-F719-4ED9-A024-14E4B8D40883}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Imaging.SkiaSharp", "src\Volo.Abp.Imaging.SkiaSharp\Volo.Abp.Imaging.SkiaSharp.csproj", "{198683D0-7DC6-40F2-B81B-8E446E70A9DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Imaging.SkiaSharp.Tests", "test\Volo.Abp.Imaging.SkiaSharp.Tests\Volo.Abp.Imaging.SkiaSharp.Tests.csproj", "{DFAF8763-D1D6-4EB4-B459-20E31007FE2F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1369,6 +1373,14 @@ Global {F19A6E0C-F719-4ED9-A024-14E4B8D40883}.Debug|Any CPU.Build.0 = Debug|Any CPU {F19A6E0C-F719-4ED9-A024-14E4B8D40883}.Release|Any CPU.ActiveCfg = Release|Any CPU {F19A6E0C-F719-4ED9-A024-14E4B8D40883}.Release|Any CPU.Build.0 = Release|Any CPU + {198683D0-7DC6-40F2-B81B-8E446E70A9DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {198683D0-7DC6-40F2-B81B-8E446E70A9DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {198683D0-7DC6-40F2-B81B-8E446E70A9DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {198683D0-7DC6-40F2-B81B-8E446E70A9DE}.Release|Any CPU.Build.0 = Release|Any CPU + {DFAF8763-D1D6-4EB4-B459-20E31007FE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFAF8763-D1D6-4EB4-B459-20E31007FE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFAF8763-D1D6-4EB4-B459-20E31007FE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFAF8763-D1D6-4EB4-B459-20E31007FE2F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1600,6 +1612,8 @@ Global {62B2B8C9-8F24-4D31-894F-C1F0728D32AB} = {447C8A77-E5F0-4538-8687-7383196D04EA} {983B0136-384B-4439-B374-31111FFAA286} = {447C8A77-E5F0-4538-8687-7383196D04EA} {F19A6E0C-F719-4ED9-A024-14E4B8D40883} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {198683D0-7DC6-40F2-B81B-8E446E70A9DE} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {DFAF8763-D1D6-4EB4-B459-20E31007FE2F} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xml b/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xsd b/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj new file mode 100644 index 0000000000..1ebb168c84 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj @@ -0,0 +1,26 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0 + enable + Nullable + Volo.Abp.Imaging.SkiaSharp + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/AbpImagingSkiaSharpModule.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/AbpImagingSkiaSharpModule.cs new file mode 100644 index 0000000000..79fc4314aa --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/AbpImagingSkiaSharpModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Imaging; + +[DependsOn(typeof(AbpImagingAbstractionsModule))] +public class AbpImagingSkiaSharpModule : AbpModule +{ +} diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs new file mode 100644 index 0000000000..760acd0916 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using SkiaSharp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; + +namespace Volo.Abp.Imaging; + +public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITransientDependency +{ + protected SkiaSharpResizerOptions Options { get; } + + public SkiaSharpImageResizerContributor(IOptions options) + { + Options = options.Value; + } + + public virtual async Task> TryResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, string? mimeType = null, CancellationToken cancellationToken = default) + { + if (!mimeType.IsNullOrWhiteSpace() && !CanResize(mimeType)) + { + return new ImageResizeResult(bytes, ImageProcessState.Unsupported); + } + + using (var memoryStream = new MemoryStream(bytes)) + { + var result = await TryResizeAsync(memoryStream, resizeArgs, mimeType, cancellationToken); + + if (result.State != ImageProcessState.Done) + { + return new ImageResizeResult(bytes, result.State); + } + + var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); + + result.Result.Dispose(); + + return new ImageResizeResult(newBytes, result.State); + } + } + + public virtual async Task> TryResizeAsync(Stream stream, ImageResizeArgs resizeArgs, string? mimeType = null, CancellationToken cancellationToken = default) + { + if (!mimeType.IsNullOrWhiteSpace() && !CanResize(mimeType)) + { + return new ImageResizeResult(stream, ImageProcessState.Unsupported); + } + + var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream); + + using (var original = SKBitmap.Decode(memoryBitmapStream)) + { + using (var resized = original.Resize(new SKImageInfo(resizeArgs.Width, resizeArgs.Height), Options.SKFilterQuality)) + { + using (var image = SKImage.FromBitmap(resized)) + { + using (var codec = SKCodec.Create(memorySkCodecStream)) + { + var memoryStream = new MemoryStream(); + image.Encode(codec.EncodedFormat, Options.Quality).SaveTo(memoryStream); + return new ImageResizeResult(memoryStream, ImageProcessState.Done); + } + } + } + } + } + + protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream) + { + var streamPosition = stream.Position; + + var memoryBitmapStream = new MemoryStream(); + var memorySkCodecStream = new MemoryStream(); + + await stream.CopyToAsync(memoryBitmapStream); + stream.Position = streamPosition; + await stream.CopyToAsync(memorySkCodecStream); + stream.Position = streamPosition; + + memoryBitmapStream.Position = 0; + memorySkCodecStream.Position = 0; + + return (memoryBitmapStream, memorySkCodecStream); + } + + protected virtual bool CanResize(string? mimeType) + { + return mimeType switch { + MimeTypes.Image.Jpeg => true, + MimeTypes.Image.Png => true, + MimeTypes.Image.Webp => true, + _ => false + }; + } +} diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpResizerOptions.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpResizerOptions.cs new file mode 100644 index 0000000000..6bc220feb1 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpResizerOptions.cs @@ -0,0 +1,16 @@ +using SkiaSharp; + +namespace Volo.Abp.Imaging; + +public class SkiaSharpResizerOptions +{ + public SKFilterQuality SKFilterQuality { get; set; } + + public int Quality { get; set; } + + public SkiaSharpResizerOptions() + { + SKFilterQuality = SKFilterQuality.None; + Quality = 75; + } +} diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo.Abp.Imaging.SkiaSharp.Tests.csproj b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo.Abp.Imaging.SkiaSharp.Tests.csproj new file mode 100644 index 0000000000..3272fc325a --- /dev/null +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo.Abp.Imaging.SkiaSharp.Tests.csproj @@ -0,0 +1,19 @@ + + + + + + net8.0 + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingMagickNetTestModule.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingMagickNetTestModule.cs new file mode 100644 index 0000000000..7fce00d3ac --- /dev/null +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingMagickNetTestModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Imaging; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpImagingSkiaSharpModule), + typeof(AbpTestBaseModule) +)] +public class AbpImagingSkiaSharpTestModule : AbpModule +{ + +} diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingSkiaSharpTestModule.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingSkiaSharpTestModule.cs new file mode 100644 index 0000000000..953e8c4b3a --- /dev/null +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/AbpImagingSkiaSharpTestModule.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.Imaging; + +public abstract class AbpImagingSkiaSharpTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs new file mode 100644 index 0000000000..460c83bf5b --- /dev/null +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs @@ -0,0 +1,75 @@ +using System.IO; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Imaging; + +public class SkiaSharpImageResizerTests : AbpImagingSkiaSharpTestBase +{ + public IImageResizer ImageResizer { get; } + + public SkiaSharpImageResizerTests() + { + ImageResizer = GetRequiredService(); + } + + [Fact] + public async Task Should_Resize_Jpg() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Done); + resizedImage.Result.Length.ShouldBeLessThan(jpegImage.Length); + + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Resize_Png() + { + await using var pngImage = ImageFileHelper.GetPngTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(pngImage, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Done); + resizedImage.Result.Length.ShouldBeLessThan(pngImage.Length); + + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Resize_Webp() + { + await using var webpImage = ImageFileHelper.GetWebpTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(webpImage, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Done); + resizedImage.Result.Length.ShouldBeLessThan(webpImage.Length); + + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Resize_Stream_And_Byte_Array_The_Same() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage1 = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(100, 100)); + var resizedImage2 = await ImageResizer.ResizeAsync(await jpegImage.GetAllBytesAsync(), new ImageResizeArgs(100, 100)); + + resizedImage1.ShouldNotBeNull(); + resizedImage1.State.ShouldBe(ImageProcessState.Done); + resizedImage1.Result.Length.ShouldBeLessThan(jpegImage.Length); + + resizedImage2.ShouldNotBeNull(); + resizedImage2.State.ShouldBe(ImageProcessState.Done); + resizedImage2.Result.LongLength.ShouldBeLessThan(jpegImage.Length); + + resizedImage1.Result.Length.ShouldBe(resizedImage2.Result.LongLength); + + resizedImage1.Result.Dispose(); + } +} diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 96d61dc74d..78de2aef44 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -204,6 +204,7 @@ $projects = ( "framework/src/Volo.Abp.Imaging.AspNetCore", "framework/src/Volo.Abp.Imaging.ImageSharp", "framework/src/Volo.Abp.Imaging.MagickNet", + "framework/src/Volo.Abp.Imaging.SkiaSharp", "framework/src/Volo.Abp.Json", "framework/src/Volo.Abp.Json.Abstractions", "framework/src/Volo.Abp.Json.Newtonsoft", From bdc0e92f33a5db02c686d4e6b9e38d8923445841 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 13 Nov 2023 09:40:54 +0800 Subject: [PATCH 2/2] Add `SkiaSharp`'s linux and macos assets. --- Directory.Packages.props | 2 ++ .../Volo.Abp.Imaging.SkiaSharp.csproj | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8a2b1c786c..a6d8cfdc4f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -141,6 +141,8 @@ + + diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj index 1ebb168c84..857cb0f92f 100644 --- a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo.Abp.Imaging.SkiaSharp.csproj @@ -21,6 +21,8 @@ + +