diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 1c201237e3..b773ba72de 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -439,6 +439,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Ldap.Abstractions" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Ddd.Domain.Shared", "src\Volo.Abp.Ddd.Domain.Shared\Volo.Abp.Ddd.Domain.Shared.csproj", "{0858571B-CE73-4AD6-BD06-EC9F0714D8E9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Image.Abstractions", "src\Volo.Abp.Image.Abstractions\Volo.Abp.Image.Abstractions.csproj", "{32F3E84B-D02E-42BD-BC5C-0D211564EF30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Image", "src\Volo.Abp.Image\Volo.Abp.Image.csproj", "{3F9E50EF-09E4-4548-8E70-815ED946CEA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Image.Web", "src\Volo.Abp.Image.Web\Volo.Abp.Image.Web.csproj", "{78340A37-219E-4F2D-9AC6-40A7B467EEEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Image.ImageSharp", "src\Volo.Abp.Image.ImageSharp\Volo.Abp.Image.ImageSharp.csproj", "{44467427-E0BE-492C-B9B4-82B362C183C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Image.MagickNet", "src\Volo.Abp.Image.MagickNet\Volo.Abp.Image.MagickNet.csproj", "{F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1309,6 +1319,26 @@ Global {0858571B-CE73-4AD6-BD06-EC9F0714D8E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0858571B-CE73-4AD6-BD06-EC9F0714D8E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0858571B-CE73-4AD6-BD06-EC9F0714D8E9}.Release|Any CPU.Build.0 = Release|Any CPU + {32F3E84B-D02E-42BD-BC5C-0D211564EF30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32F3E84B-D02E-42BD-BC5C-0D211564EF30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32F3E84B-D02E-42BD-BC5C-0D211564EF30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32F3E84B-D02E-42BD-BC5C-0D211564EF30}.Release|Any CPU.Build.0 = Release|Any CPU + {3F9E50EF-09E4-4548-8E70-815ED946CEA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F9E50EF-09E4-4548-8E70-815ED946CEA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F9E50EF-09E4-4548-8E70-815ED946CEA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F9E50EF-09E4-4548-8E70-815ED946CEA4}.Release|Any CPU.Build.0 = Release|Any CPU + {78340A37-219E-4F2D-9AC6-40A7B467EEEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78340A37-219E-4F2D-9AC6-40A7B467EEEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78340A37-219E-4F2D-9AC6-40A7B467EEEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78340A37-219E-4F2D-9AC6-40A7B467EEEC}.Release|Any CPU.Build.0 = Release|Any CPU + {44467427-E0BE-492C-B9B4-82B362C183C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44467427-E0BE-492C-B9B4-82B362C183C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44467427-E0BE-492C-B9B4-82B362C183C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44467427-E0BE-492C-B9B4-82B362C183C3}.Release|Any CPU.Build.0 = Release|Any CPU + {F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1530,6 +1560,11 @@ Global {8764DFAF-D13D-449A-9A5E-5D7F0B2D7FEF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {0F80E95C-41E6-4F23-94FF-FC9D0B8D5D71} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {0858571B-CE73-4AD6-BD06-EC9F0714D8E9} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {3F9E50EF-09E4-4548-8E70-815ED946CEA4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {32F3E84B-D02E-42BD-BC5C-0D211564EF30} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {44467427-E0BE-492C-B9B4-82B362C183C3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {F701EDA5-D7EA-4AA7-9C57-83ED50CE72EC} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {78340A37-219E-4F2D-9AC6-40A7B467EEEC} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.Core/System/IO/AbpStreamExtensions.cs b/framework/src/Volo.Abp.Core/System/IO/AbpStreamExtensions.cs index d39e5b844b..110b828dbe 100644 --- a/framework/src/Volo.Abp.Core/System/IO/AbpStreamExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/IO/AbpStreamExtensions.cs @@ -7,28 +7,28 @@ public static class AbpStreamExtensions { public static byte[] GetAllBytes(this Stream stream) { - using (var memoryStream = new MemoryStream()) + if (stream is MemoryStream memoryStream) { - if (stream.CanSeek) - { - stream.Position = 0; - } - stream.CopyTo(memoryStream); return memoryStream.ToArray(); } + + using (var ms = stream.CreateMemoryStream()) + { + return ms.ToArray(); + } } public static async Task GetAllBytesAsync(this Stream stream, CancellationToken cancellationToken = default) { - using (var memoryStream = new MemoryStream()) + if (stream is MemoryStream memoryStream) { - if (stream.CanSeek) - { - stream.Position = 0; - } - await stream.CopyToAsync(memoryStream, cancellationToken); return memoryStream.ToArray(); } + + using (var ms = await stream.CreateMemoryStreamAsync(cancellationToken)) + { + return ms.ToArray(); + } } public static Task CopyToAsync(this Stream stream, Stream destination, CancellationToken cancellationToken) @@ -43,4 +43,36 @@ public static class AbpStreamExtensions cancellationToken ); } + + public async static Task CreateMemoryStreamAsync(this Stream stream, CancellationToken cancellationToken = default) + { + if (stream.CanSeek) + { + stream.Position = 0; + } + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken); + if (stream.CanSeek) + { + stream.Position = 0; + } + memoryStream.Position = 0; + return memoryStream; + } + + public static MemoryStream CreateMemoryStream(this Stream stream) + { + if (stream.CanSeek) + { + stream.Position = 0; + } + var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + if (stream.CanSeek) + { + stream.Position = 0; + } + memoryStream.Position = 0; + return memoryStream; + } } diff --git a/framework/src/Volo.Abp.Image.Abstractions/FodyWeavers.xml b/framework/src/Volo.Abp.Image.Abstractions/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/FodyWeavers.xsd b/framework/src/Volo.Abp.Image.Abstractions/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/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.Image.Abstractions/Volo.Abp.Image.Abstractions.csproj b/framework/src/Volo.Abp.Image.Abstractions/Volo.Abp.Image.Abstractions.csproj new file mode 100644 index 0000000000..2a6a0000e9 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo.Abp.Image.Abstractions.csproj @@ -0,0 +1,20 @@ + + + + + + + netstandard2.0;netstandard2.1;net7.0 + Volo.Abp.Image.Abstractions + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo.Abp.Json.abppkg.json b/framework/src/Volo.Abp.Image.Abstractions/Volo.Abp.Json.abppkg.json new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo.Abp.Json.abppkg.json @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/AbpImageAbstractionsModule.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/AbpImageAbstractionsModule.cs new file mode 100644 index 0000000000..16d4ddfd6f --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/AbpImageAbstractionsModule.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Image; + +public class AbpImageAbstractionsModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressor.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressor.cs new file mode 100644 index 0000000000..610b320c46 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressor.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Image; + +public interface IImageCompressor +{ + Task CompressAsync(Stream stream, CancellationToken cancellationToken = default); + + Stream Compress(Stream stream); + + bool CanCompress(IImageFormat imageFormat); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressorSelector.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressorSelector.cs new file mode 100644 index 0000000000..5a447f22dd --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageCompressorSelector.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Image; + +public interface IImageCompressorSelector +{ + IImageCompressor FindCompressor(IImageFormat imageFormat); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormat.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormat.cs new file mode 100644 index 0000000000..76d57ab922 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormat.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Image; + +public interface IImageFormat +{ + string Name { get; } + string MimeType { get; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormatDetector.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormatDetector.cs new file mode 100644 index 0000000000..b085706645 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageFormatDetector.cs @@ -0,0 +1,8 @@ +using System.IO; + +namespace Volo.Abp.Image; + +public interface IImageFormatDetector +{ + IImageFormat FindFormat(Stream image); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizeParameter.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizeParameter.cs new file mode 100644 index 0000000000..874c9f95b2 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizeParameter.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Image; + +public interface IImageResizeParameter +{ + int? Width { get; } + int? Height { get; } + + ImageResizeMode? Mode { get; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizer.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizer.cs new file mode 100644 index 0000000000..a0ed3e161b --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizer.cs @@ -0,0 +1,15 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Image; + +public interface IImageResizer +{ + Task ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, + CancellationToken cancellationToken = default); + + Stream Resize(Stream stream, IImageResizeParameter resizeParameter); + + bool CanResize(IImageFormat imageFormat); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizerSelector.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizerSelector.cs new file mode 100644 index 0000000000..92c1fa11be --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/IImageResizerSelector.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Image; + +public interface IImageResizerSelector +{ + IImageResizer FindResizer(IImageFormat imageFormat); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageFormat.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageFormat.cs new file mode 100644 index 0000000000..ddf7aa6d4d --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageFormat.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.Image; + +public class ImageFormat : IImageFormat +{ + public ImageFormat(string name, string mimeType) + { + Name = name; + MimeType = mimeType; + } + + public string Name { get; } + public string MimeType { get; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageResizeMode.cs b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageResizeMode.cs new file mode 100644 index 0000000000..5f4f73de87 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Abstractions/Volo/Abp/Image/ImageResizeMode.cs @@ -0,0 +1,15 @@ +namespace Volo.Abp.Image; + +public enum ImageResizeMode +{ + None, + Stretch, + BoxPad, + Min, + Max, + Crop, + Pad, + Fill, + Distort, + Default = Stretch +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/FodyWeavers.xml b/framework/src/Volo.Abp.Image.ImageSharp/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/FodyWeavers.xsd b/framework/src/Volo.Abp.Image.ImageSharp/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/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.Image.ImageSharp/Volo.Abp.Image.ImageSharp.csproj b/framework/src/Volo.Abp.Image.ImageSharp/Volo.Abp.Image.ImageSharp.csproj new file mode 100644 index 0000000000..c167eb558b --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo.Abp.Image.ImageSharp.csproj @@ -0,0 +1,24 @@ + + + + + + + netstandard2.0; + Volo.Abp.Image.ImageSharp + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/Volo.Abp.Json.abppkg.json b/framework/src/Volo.Abp.Image.ImageSharp/Volo.Abp.Json.abppkg.json new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo.Abp.Json.abppkg.json @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/AbpImageSharpModule.cs b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/AbpImageSharpModule.cs new file mode 100644 index 0000000000..d148f64f4f --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/AbpImageSharpModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Image; + +[DependsOn(typeof(AbpImageAbstractionsModule))] +public class AbpImageSharpModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageCompressor.cs b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageCompressor.cs new file mode 100644 index 0000000000..dbb2f27e25 --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageCompressor.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class ImageSharpImageCompressor : IImageCompressor, ITransientDependency +{ + public async Task CompressAsync(Stream stream, CancellationToken cancellationToken = default) + { + var newStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); + using var image = await SixLabors.ImageSharp.Image.LoadAsync(newStream, cancellationToken); + newStream.Position = 0; + var format = await SixLabors.ImageSharp.Image.DetectFormatAsync(newStream, cancellationToken); + newStream.Position = 0; + var encoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(format); + + switch (encoder) + { + case JpegEncoder jpegEncoder: + jpegEncoder.Quality = 60; + break; + case PngEncoder pngEncoder: + pngEncoder.CompressionLevel = PngCompressionLevel.BestCompression; + pngEncoder.IgnoreMetadata = true; + break; + case WebpEncoder webPEncoder: + webPEncoder.Quality = 60; + webPEncoder.UseAlphaCompression = true; + break; + case null: + throw new NotSupportedException($"No encoder available for provided path: {format.Name}"); + } + + await image.SaveAsync(newStream, encoder, cancellationToken: cancellationToken); + newStream.SetLength(newStream.Position); + return newStream; + } + + public Stream Compress(Stream stream) + { + var newStream = stream.CreateMemoryStream(); + using var image = SixLabors.ImageSharp.Image.Load(newStream); + newStream.Position = 0; + var format = SixLabors.ImageSharp.Image.DetectFormat(newStream); + newStream.Position = 0; + var encoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(format); + + switch (encoder) + { + case JpegEncoder jpegEncoder: + jpegEncoder.Quality = 60; + break; + case PngEncoder pngEncoder: + pngEncoder.CompressionLevel = PngCompressionLevel.BestCompression; + pngEncoder.IgnoreMetadata = true; + break; + case WebpEncoder webPEncoder: + webPEncoder.Quality = 60; + webPEncoder.UseAlphaCompression = true; + break; + case null: + throw new NotSupportedException($"No encoder available for provided path: {format.Name}"); + } + + image.Save(newStream, encoder); + newStream.SetLength(newStream.Position); + return newStream; + } + + + public bool CanCompress(IImageFormat format) + { + return format?.MimeType switch { + "image/jpeg" => true, + "image/png" => true, + "image/webp" => true, + _ => false + }; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageFormatDetector.cs b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageFormatDetector.cs new file mode 100644 index 0000000000..f668fbe000 --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageFormatDetector.cs @@ -0,0 +1,13 @@ +using System.IO; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class ImageSharpImageFormatDetector : IImageFormatDetector, ITransientDependency +{ + public IImageFormat FindFormat(Stream stream) + { + var format = SixLabors.ImageSharp.Image.DetectFormat(stream); + return new ImageFormat(format.Name, format.DefaultMimeType); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageResizer.cs b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageResizer.cs new file mode 100644 index 0000000000..ff20fae798 --- /dev/null +++ b/framework/src/Volo.Abp.Image.ImageSharp/Volo/Abp/Image/ImageSharpImageResizer.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class ImageSharpImageResizer : IImageResizer, ITransientDependency +{ + + public async Task ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, CancellationToken cancellationToken = default) + { + var newStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); + using var image = await SixLabors.ImageSharp.Image.LoadAsync(newStream, cancellationToken); + ApplyMode(image, resizeParameter); + newStream.Position = 0; + var format = await SixLabors.ImageSharp.Image.DetectFormatAsync(newStream, cancellationToken); + newStream.Position = 0; + await image.SaveAsync(newStream, format, cancellationToken: cancellationToken); + newStream.SetLength(newStream.Position); + newStream.Position = 0; + return newStream; + } + + public Stream Resize(Stream stream, IImageResizeParameter resizeParameter) + { + var newStream = stream.CreateMemoryStream(); + using var image = SixLabors.ImageSharp.Image.Load(newStream); + ApplyMode(image, resizeParameter); + newStream.Position = 0; + var format = SixLabors.ImageSharp.Image.DetectFormat(newStream); + newStream.Position = 0; + image.Save(newStream, format); + newStream.SetLength(newStream.Position); + newStream.Position = 0; + return newStream; + } + + public bool CanResize(IImageFormat imageFormat) + { + return imageFormat?.MimeType switch + { + "image/jpeg" => true, + "image/png" => true, + "image/gif" => true, + "image/bmp" => true, + "image/tiff" => true, + _ => false + }; + } + + private void ApplyMode(SixLabors.ImageSharp.Image image, IImageResizeParameter resizeParameter) + { + var width = resizeParameter.Width ?? image.Width; + var height = resizeParameter.Height ?? image.Height; + + var defaultResizeOptions = new ResizeOptions { Size = new SixLabors.ImageSharp.Size(width, height) }; + + switch (resizeParameter.Mode) + { + case null: + case ImageResizeMode.None: + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Stretch: + defaultResizeOptions.Mode = ResizeMode.Stretch; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.BoxPad: + defaultResizeOptions.Mode = ResizeMode.BoxPad; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Min: + defaultResizeOptions.Mode = ResizeMode.Min; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Max: + defaultResizeOptions.Mode = ResizeMode.Max; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Crop: + defaultResizeOptions.Mode = ResizeMode.Crop; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Pad: + defaultResizeOptions.Mode = ResizeMode.Pad; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + case ImageResizeMode.Fill: + defaultResizeOptions.Mode = ResizeMode.Stretch; + image.Mutate(x => x.Resize(defaultResizeOptions)); + break; + default: + throw new NotSupportedException("Resize mode " + resizeParameter.Mode + "is not supported!"); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/FodyWeavers.xml b/framework/src/Volo.Abp.Image.MagickNet/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/FodyWeavers.xsd b/framework/src/Volo.Abp.Image.MagickNet/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/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.Image.MagickNet/Volo.Abp.Image.MagickNet.csproj b/framework/src/Volo.Abp.Image.MagickNet/Volo.Abp.Image.MagickNet.csproj new file mode 100644 index 0000000000..af614bf983 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo.Abp.Image.MagickNet.csproj @@ -0,0 +1,24 @@ + + + + + + + netstandard2.0;netstandard2.1;net7.0 + Volo.Abp.Image.MagicNet + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/Volo.Abp.Json.abppkg.json b/framework/src/Volo.Abp.Image.MagickNet/Volo.Abp.Json.abppkg.json new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo.Abp.Json.abppkg.json @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/AbpImageMagickNetModule.cs b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/AbpImageMagickNetModule.cs new file mode 100644 index 0000000000..15e17f2954 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/AbpImageMagickNetModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Image; + +[DependsOn(typeof(AbpImageAbstractionsModule))] +public class AbpImageMagickNetModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageCompressor.cs b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageCompressor.cs new file mode 100644 index 0000000000..fa96b026fb --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageCompressor.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ImageMagick; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class MagickImageCompressor : IImageCompressor, ITransientDependency +{ + private readonly ImageOptimizer _optimizer = new(); + + public async Task CompressAsync(Stream stream, CancellationToken cancellationToken = default) + { + var newStream = await stream.CreateMemoryStreamAsync(cancellationToken:cancellationToken); + try + { + _optimizer.IsSupported(newStream); + newStream.Position = 0; + _optimizer.Compress(newStream); + } + catch + { + // ignored + } + return newStream; + } + + public Stream Compress(Stream stream) + { + var newStream = stream.CreateMemoryStream(); + try + { + _optimizer.IsSupported(newStream); + newStream.Position = 0; + _optimizer.Compress(newStream); + } + catch + { + // ignored + } + return newStream; + } + + public bool CanCompress(IImageFormat imageFormat) + { + return imageFormat?.MimeType switch + { + "image/jpeg" => true, + "image/png" => true, + "image/gif" => true, + "image/bmp" => true, + "image/tiff" => true, + _ => false + }; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageFormatDetector.cs b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageFormatDetector.cs new file mode 100644 index 0000000000..cb84e30023 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageFormatDetector.cs @@ -0,0 +1,15 @@ +using System.IO; +using ImageMagick; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class MagickImageFormatDetector : IImageFormatDetector, ITransientDependency +{ + public IImageFormat FindFormat(Stream image) + { + using var magickImage = new MagickImage(image); + var format = magickImage.FormatInfo; + return format == null ? null : new ImageFormat(format.Format.ToString(), format.MimeType ?? string.Empty); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageResizer.cs b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageResizer.cs new file mode 100644 index 0000000000..5fab17e825 --- /dev/null +++ b/framework/src/Volo.Abp.Image.MagickNet/Volo/Abp/Image/MagickImageResizer.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ImageMagick; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class MagickImageResizer : IImageResizer, ITransientDependency +{ + public async Task ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, + CancellationToken cancellationToken = default) + { + var newStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); + try + { + using var image = new MagickImage(newStream); + ApplyMode(image, resizeParameter); + newStream.Position = 0; + await image.WriteAsync(newStream, cancellationToken); + newStream.SetLength(newStream.Position); + newStream.Position = 0; + } + catch + { + // ignored + } + + return newStream; + } + + public Stream Resize(Stream stream, IImageResizeParameter resizeParameter) + { + var newStream = stream.CreateMemoryStream(); + try + { + using var image = new MagickImage(newStream); + ApplyMode(image, resizeParameter); + newStream.Position = 0; + image.Write(newStream); + newStream.SetLength(newStream.Position); + newStream.Position = 0; + } + catch + { + // ignored + } + + return newStream; + } + + public bool CanResize(IImageFormat imageFormat) + { + return imageFormat?.MimeType switch { + "image/jpeg" => true, + "image/png" => true, + "image/gif" => true, + "image/bmp" => true, + "image/tiff" => true, + _ => false + }; + } + + private void ApplyMode(IMagickImage image, IImageResizeParameter resizeParameter) + { + var width = resizeParameter.Width ?? image.Width; + var height = resizeParameter.Height ?? image.Height; + var defaultMagickGeometry = new MagickGeometry(width, height); + var imageRatio = image.Height / (float)image.Width; + var percentHeight = Math.Abs(height / (float)image.Height); + var percentWidth = Math.Abs(width / (float)image.Width); + var ratio = height / (float)width; + var newWidth = width; + var newHeight = height; + switch (resizeParameter.Mode) + { + case null: + case ImageResizeMode.None: + image.Resize(defaultMagickGeometry); + break; + case ImageResizeMode.Stretch: + defaultMagickGeometry.IgnoreAspectRatio = true; + image.Resize(defaultMagickGeometry); + break; + case ImageResizeMode.Pad: + if (percentHeight < percentWidth) + { + newWidth = (int)Math.Round(image.Width * percentHeight); + } + else + { + newHeight = (int)Math.Round(image.Height * percentWidth); + } + + defaultMagickGeometry.IgnoreAspectRatio = true; + image.Resize(newWidth, newHeight); + image.Extent(width, height, Gravity.Center); + break; + case ImageResizeMode.BoxPad: + int boxPadWidth = width > 0 ? width : (int)Math.Round(image.Width * percentHeight); + int boxPadHeight = height > 0 ? height : (int)Math.Round(image.Height * percentWidth); + + if (image.Width < boxPadWidth && image.Height < boxPadHeight) + { + newWidth = boxPadWidth; + newHeight = boxPadHeight; + } + + image.Resize(newWidth, newHeight); + image.Extent(defaultMagickGeometry, Gravity.Center); + break; + case ImageResizeMode.Max: + + if (imageRatio < ratio) + { + newHeight = (int)(image.Height * percentWidth); + } + else + { + newWidth = (int)(image.Width * percentHeight); + } + + image.Resize(newWidth, newHeight); + break; + case ImageResizeMode.Min: + if (width > image.Width || height > image.Height) + { + newWidth = image.Width; + newHeight = image.Height; + } + else + { + int widthDiff = image.Width - width; + int heightDiff = image.Height - height; + + if (widthDiff > heightDiff) + { + newWidth = (int)Math.Round(height / imageRatio); + } + else if (widthDiff < heightDiff) + { + newHeight = (int)Math.Round(width * imageRatio); + } + else + { + if (height > width) + { + newHeight = (int)Math.Round(image.Height * percentWidth); + } + else + { + newHeight = (int)Math.Round(image.Height * percentWidth); + } + } + } + + image.Resize(newWidth, newHeight); + break; + case ImageResizeMode.Crop: + defaultMagickGeometry.IgnoreAspectRatio = true; + image.Crop(width, height, Gravity.Center); + image.Resize(defaultMagickGeometry); + break; + case ImageResizeMode.Distort: + image.Distort(DistortMethod.Resize, width, height); + break; + case ImageResizeMode.Fill: + defaultMagickGeometry.IgnoreAspectRatio = true; + defaultMagickGeometry.FillArea = true; + image.Resize(defaultMagickGeometry); + break; + default: + throw new NotSupportedException("Resize mode " + resizeParameter.Mode + "is not supported!"); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/FodyWeavers.xml b/framework/src/Volo.Abp.Image.Web/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/FodyWeavers.xsd b/framework/src/Volo.Abp.Image.Web/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/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.Image.Web/Volo.Abp.Image.Web.csproj b/framework/src/Volo.Abp.Image.Web/Volo.Abp.Image.Web.csproj new file mode 100644 index 0000000000..f8ce10d10c --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo.Abp.Image.Web.csproj @@ -0,0 +1,31 @@ + + + + + + + net7.0 + Volo.Abp.Image.Web + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + true + Library + true + + + + + + + + + + + true + content\ + + + + diff --git a/framework/src/Volo.Abp.Image.Web/Volo.Abp.Json.abppkg.json b/framework/src/Volo.Abp.Image.Web/Volo.Abp.Json.abppkg.json new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo.Abp.Json.abppkg.json @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageCompressAttribute.cs b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageCompressAttribute.cs new file mode 100644 index 0000000000..7935672bfe --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageCompressAttribute.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Volo.Abp.Content; + +namespace Volo.Abp.Image; + +public class AbpImageCompressActionFilterAttribute : ActionFilterAttribute +{ + public string[] Parameters { get; } + + public AbpImageCompressActionFilterAttribute(params string[] parameters) + { + Parameters = parameters; + } + + public async override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var parameters = Parameters.Any() + ? context.ActionArguments.Where(x => Parameters.Contains(x.Key)).ToArray() + : context.ActionArguments.ToArray(); + + var parameterDescriptors = context.ActionDescriptor.Parameters.OfType(); + var parameterMap = new Dictionary(); + foreach (var parameterDescriptor in parameterDescriptors) + { + var parameter = parameterDescriptor.ParameterInfo; + var attribute = parameter.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + parameterMap.Add(attribute.Parameter, parameter); + } + + foreach (var (key, value) in parameters) + { + object compressedValue = value switch { + IFormFile file => await file.CompressImageAsync(context.HttpContext.RequestServices), + IRemoteStreamContent remoteStreamContent => await remoteStreamContent.CompressImageAsync(context.HttpContext.RequestServices), + Stream stream => await stream.CompressImageAsync(context.HttpContext.RequestServices), + _ => null + }; + + if (parameterMap.TryGetValue(key, out var parameterInfo)) + { + if (parameterInfo.Name != null) + { + context.ActionArguments.Add(parameterInfo.Name, value); + } + else if (value is IDisposable disposable) + { + disposable.Dispose(); + } + } + else if (value is IDisposable disposable) + { + disposable.Dispose(); + } + + if (compressedValue != null) + { + context.ActionArguments[key] = compressedValue; + } + } + + await next(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageWebModule.cs b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageWebModule.cs new file mode 100644 index 0000000000..9121f4660b --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpImageWebModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Image; + +[DependsOn(typeof(AbpImageModule))] +public class AbpImageWebModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpOriginalImageAttribute.cs b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpOriginalImageAttribute.cs new file mode 100644 index 0000000000..e2ee6078dd --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/AbpOriginalImageAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Volo.Abp.Image; + +[AttributeUsage(AttributeTargets.Parameter)] +public class AbpOriginalImageAttribute : Attribute +{ + public AbpOriginalImageAttribute(string parameter) + { + Parameter = parameter; + } + + public string Parameter { get; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/IFormFileExtensions.cs b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/IFormFileExtensions.cs new file mode 100644 index 0000000000..3f9f39fcaf --- /dev/null +++ b/framework/src/Volo.Abp.Image.Web/Volo/Abp/Image/IFormFileExtensions.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.Image; + +public static class IFormFileExtensions +{ + public async static Task CompressImageAsync( + this IFormFile formFile, + IImageFormat imageFormat, + IImageCompressor imageCompressor, + CancellationToken cancellationToken = default) + { + if (!formFile.ContentType.StartsWith("image")) + { + return formFile; + } + + if (!imageCompressor.CanCompress(imageFormat)) + { + return formFile; + } + + var compressedImageStream = await formFile.OpenReadStream().CompressImageAsync(imageFormat, imageCompressor, cancellationToken); + + var newFormFile = new FormFile(compressedImageStream, 0, compressedImageStream.Length, formFile.Name, + formFile.FileName) { Headers = formFile.Headers }; + + return newFormFile; + } + + public async static Task CompressImageAsync(this IFormFile formFile, IServiceProvider serviceProvider) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageCompressorSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(formFile.OpenReadStream()); + return await formFile.CompressImageAsync(format, imageCompressorSelector.FindCompressor(format)); + } + + public static async Task ResizeImageAsync( + this IFormFile formFile, + IImageResizeParameter imageResizeParameter, + IImageFormat imageFormat, + IImageResizer imageResizer, + CancellationToken cancellationToken = default) + { + if (!formFile.ContentType.StartsWith("image")) + { + return formFile; + } + + if (!imageResizer.CanResize(imageFormat)) + { + return formFile; + } + + var resizedImageStream = + await formFile.OpenReadStream().ResizeImageAsync(imageResizeParameter, imageFormat, imageResizer, cancellationToken); + + var newFormFile = new FormFile(resizedImageStream, 0, resizedImageStream.Length, formFile.Name, + formFile.FileName) { Headers = formFile.Headers }; + + return newFormFile; + } + + public static Task ResizeImageAsync(this IFormFile formFile, + IImageResizeParameter imageResizeParameter, IServiceProvider serviceProvider) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageResizerSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(formFile.OpenReadStream()); + return formFile.ResizeImageAsync(imageResizeParameter, format, imageResizerSelector.FindResizer(format)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/FodyWeavers.xml b/framework/src/Volo.Abp.Image/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.Image/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/FodyWeavers.xsd b/framework/src/Volo.Abp.Image/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Image/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.Image/Volo.Abp.Image.csproj b/framework/src/Volo.Abp.Image/Volo.Abp.Image.csproj new file mode 100644 index 0000000000..3d13007579 --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo.Abp.Image.csproj @@ -0,0 +1,20 @@ + + + + + + + netstandard2.0;netstandard2.1;net7.0 + Volo.Abp.Image + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo.Abp.Json.abppkg.json b/framework/src/Volo.Abp.Image/Volo.Abp.Json.abppkg.json new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo.Abp.Json.abppkg.json @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/AbpImageModule.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/AbpImageModule.cs new file mode 100644 index 0000000000..d423f2e68d --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/AbpImageModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Image; + +[DependsOn(typeof(AbpImageAbstractionsModule))] +public class AbpImageModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/IRemoteStreamContentExtensions.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/IRemoteStreamContentExtensions.cs new file mode 100644 index 0000000000..b782aad1a3 --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/IRemoteStreamContentExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Content; + +namespace Volo.Abp.Image; + +public static class IRemoteStreamContentExtensions +{ + public async static Task CompressImageAsync(this IRemoteStreamContent remoteStreamContent, IImageFormat imageFormat, IImageCompressor imageCompressor, CancellationToken cancellationToken = default) + { + if (!imageCompressor.CanCompress(imageFormat)) + { + return remoteStreamContent; + } + + var compressedImageStream = await remoteStreamContent.GetStream().CompressImageAsync(imageFormat, imageCompressor, cancellationToken); + + return new RemoteStreamContent(compressedImageStream, remoteStreamContent.FileName, remoteStreamContent.ContentType); + } + + public async static Task CompressImageAsync(this IRemoteStreamContent remoteStreamContent, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageCompressorSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(remoteStreamContent.GetStream()); + return await remoteStreamContent.CompressImageAsync(format, imageCompressorSelector.FindCompressor(format), cancellationToken); + } + + public async static Task ResizeImageAsync(this IRemoteStreamContent remoteStreamContent, IImageResizeParameter imageResizeParameter, IImageFormat imageFormat, IImageResizer imageResizer, CancellationToken cancellationToken = default) + { + if (!imageResizer.CanResize(imageFormat)) + { + return remoteStreamContent; + } + + var resizedImageStream = await remoteStreamContent.GetStream().ResizeImageAsync(imageResizeParameter, imageFormat, imageResizer, cancellationToken); + + return new RemoteStreamContent(resizedImageStream, remoteStreamContent.FileName, remoteStreamContent.ContentType); + } + + public async static Task ResizeImageAsync(this IRemoteStreamContent remoteStreamContent, IImageResizeParameter imageResizeParameter, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageResizerSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(remoteStreamContent.GetStream()); + return await remoteStreamContent.ResizeImageAsync(imageResizeParameter, format, imageResizerSelector.FindResizer(format), cancellationToken); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageCompressorSelector.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageCompressorSelector.cs new file mode 100644 index 0000000000..2f374e860c --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageCompressorSelector.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class ImageCompressorSelector : IImageCompressorSelector, ITransientDependency +{ + private readonly IEnumerable _imageCompressors; + + public ImageCompressorSelector(IEnumerable imageCompressors) + { + _imageCompressors = imageCompressors; + } + + public IImageCompressor FindCompressor(IImageFormat imageFormat) + { + return _imageCompressors.FirstOrDefault(x => x.CanCompress(imageFormat)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizeParameter.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizeParameter.cs new file mode 100644 index 0000000000..ed5743c103 --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizeParameter.cs @@ -0,0 +1,15 @@ +namespace Volo.Abp.Image; + +public class ImageResizeParameter : IImageResizeParameter +{ + public ImageResizeParameter(int? width = null, int? height = null, ImageResizeMode? mode = null) + { + Mode = mode; + Width = width; + Height = height; + } + + public int? Width { get; set; } + public int? Height { get; set; } + public ImageResizeMode? Mode { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizerSelector.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizerSelector.cs new file mode 100644 index 0000000000..4d0a4a93a0 --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/ImageResizerSelector.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Image; + +public class ImageResizerSelector : IImageResizerSelector, ITransientDependency +{ + private readonly IEnumerable _imageResizers; + + public ImageResizerSelector(IEnumerable imageResizers) + { + _imageResizers = imageResizers; + } + + public IImageResizer FindResizer(IImageFormat imageFormat) + { + return _imageResizers.FirstOrDefault(x => x.CanResize(imageFormat)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Image/Volo/Abp/Image/StreamExtensions.cs b/framework/src/Volo.Abp.Image/Volo/Abp/Image/StreamExtensions.cs new file mode 100644 index 0000000000..7042a4fa7a --- /dev/null +++ b/framework/src/Volo.Abp.Image/Volo/Abp/Image/StreamExtensions.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.Image; + +public static class StreamExtensions +{ + public static Stream CompressImage(this Stream stream, IImageFormat imageFormat, IImageCompressor imageCompressor) + { + if (!imageCompressor.CanCompress(imageFormat)) + { + return stream; + } + + return imageCompressor.Compress(stream); + } + + public static Stream CompressImage(this Stream stream, IServiceProvider serviceProvider) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageCompressorSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(stream); + return stream.CompressImage(format, imageCompressorSelector.FindCompressor(format)); + } + + public static Task CompressImageAsync(this Stream stream, IImageFormat imageFormat, IImageCompressor imageCompressor, CancellationToken cancellationToken = default) + { + if (!imageCompressor.CanCompress(imageFormat)) + { + return Task.FromResult(stream); + } + + return imageCompressor.CompressAsync(stream, cancellationToken); + } + + public static Task CompressImageAsync(this Stream stream, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageCompressorSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(stream); + return stream.CompressImageAsync(format, imageCompressorSelector.FindCompressor(format), cancellationToken); + } + + public static Stream ResizeImage(this Stream stream, IImageResizeParameter imageResizeParameter, IImageFormat imageFormat, IImageResizer imageResizer) + { + if (!imageResizer.CanResize(imageFormat)) + { + return stream; + } + + return imageResizer.Resize(stream, imageResizeParameter); + } + + public static Stream ResizeImage(this Stream stream, IImageResizeParameter imageResizeParameter, IServiceProvider serviceProvider) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageResizerSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(stream); + return stream.ResizeImage(imageResizeParameter, format, imageResizerSelector.FindResizer(format)); + } + + public static Task ResizeImageAsync(this Stream stream, IImageResizeParameter imageResizeParameter, IImageFormat imageFormat, IImageResizer imageResizer, CancellationToken cancellationToken = default) + { + if (!imageResizer.CanResize(imageFormat)) + { + return Task.FromResult(stream); + } + + return imageResizer.ResizeAsync(stream, imageResizeParameter, cancellationToken); + } + + public static Task ResizeImageAsync(this Stream stream, IImageResizeParameter imageResizeParameter, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) + { + var imageFormatDetector = serviceProvider.GetRequiredService(); + var imageResizerSelector = serviceProvider.GetRequiredService(); + var format = imageFormatDetector.FindFormat(stream); + return stream.ResizeImageAsync(imageResizeParameter, format, imageResizerSelector.FindResizer(format), cancellationToken); + } +} \ No newline at end of file