diff --git a/Directory.Packages.props b/Directory.Packages.props
index 69d1cf0a70..c3cf4389e5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -55,7 +55,7 @@
-
+
diff --git a/docs/en/framework/infrastructure/image-manipulation.md b/docs/en/framework/infrastructure/image-manipulation.md
index 49a2e74c46..587fab07c7 100644
--- a/docs/en/framework/infrastructure/image-manipulation.md
+++ b/docs/en/framework/infrastructure/image-manipulation.md
@@ -1,12 +1,12 @@
```json
//[doc-seo]
{
- "Description": "Learn how to efficiently compress and resize images in your applications using ABP Framework's extensible services powered by ImageSharp and Magick.NET."
+ "Description": "Learn how to efficiently compress and resize images in your applications using ABP Framework's extensible services powered by ImageSharp, Magick.NET and SkiaSharp."
}
```
# Image Manipulation
-ABP provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/) and [Magick.NET](https://github.com/dlemstra/Magick.NET) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library.
+ABP provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/), [Magick.NET](https://github.com/dlemstra/Magick.NET) and [SkiaSharp](https://github.com/mono/SkiaSharp) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library.
> The image resizer/compressor system is designed to be extensible. You can implement your own image resizer/compressor contributor and use it in your application.
@@ -46,10 +46,11 @@ public class YourModule : AbpModule
## Providers
-ABP provides two image resizer/compressor implementations out of the box:
+ABP provides three image resizer/compressor implementations out of the box:
* [Magick.NET](#magick-net-provider)
* [ImageSharp](#imagesharp-provider)
+* [SkiaSharp](#skiasharp-provider)
You should install one of these provides to make it actually working.
@@ -334,6 +335,67 @@ Configure(options =>
});
```
+## SkiaSharp Provider
+
+`Volo.Abp.Imaging.SkiaSharp` NuGet package implements the image operations using the [SkiaSharp](https://github.com/mono/SkiaSharp) library.
+
+## Installation
+
+You can add this package to your application by either using the [ABP CLI](../../cli) or manually installing it. Using the [ABP CLI](../../cli) is the recommended approach.
+
+### Using the ABP CLI
+
+Open a command line terminal in the folder of your project (.csproj file) and type the following command:
+
+```bash
+abp add-package Volo.Abp.Imaging.SkiaSharp
+```
+
+### Manual Installation
+
+If you want to manually install;
+
+1. Add the [Volo.Abp.Imaging.SkiaSharp](https://www.nuget.org/packages/Volo.Abp.Imaging.SkiaSharp) NuGet package to your project:
+
+```
+dotnet add package Volo.Abp.Imaging.SkiaSharp
+```
+
+2. Add `AbpImagingSkiaSharpModule` to your [module](../architecture/modularity/basics.md)'s dependency list:
+
+```csharp
+[DependsOn(typeof(AbpImagingSkiaSharpModule))]
+public class MyModule : AbpModule
+{
+ //...
+}
+```
+
+### Configuration
+
+`SkiaSharpResizerOptions` is an [options object](../fundamentals/options.md) that is used to configure the SkiaSharp image resize system. It has the following properties:
+
+* `SKSamplingOptions`: The sampling options used by SkiaSharp when resizing. (Default: `SKSamplingOptions.Default`)
+* `Quality`: The quality of the encoded image (0-100). (Default: `75`)
+
+`SkiaSharpCompressOptions` is an [options object](../fundamentals/options.md) that is used to configure the SkiaSharp image compression system. It has the following properties:
+
+* `Quality`: The quality of the encoded image (0-100). (Default: `75`)
+
+**Example usage:**
+
+```csharp
+Configure(options =>
+{
+ options.Quality = 80;
+});
+
+Configure(options =>
+{
+ options.Quality = 60;
+});
+```
+
## ASP.NET Core Integration
`Volo.Abp.Imaging.AspNetCore` NuGet package defines attributes for controller actions that can automatically compress and/or resize uploaded files.
diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md
index 15fc138dc8..84a10fafe9 100644
--- a/docs/en/package-version-changes.md
+++ b/docs/en/package-version-changes.md
@@ -11,6 +11,7 @@
| Package | Old Version | New Version | PR |
|---------|-------------|-------------|-----|
+| Magick.NET-Q16-AnyCPU | 14.9.1 | 14.13.0 | #25427 |
| MongoDB.Driver | 3.8.0 | 3.8.1 | #25404 |
## 10.4.0-rc.1
diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs
new file mode 100644
index 0000000000..1c7e029807
--- /dev/null
+++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs
@@ -0,0 +1,11 @@
+namespace Volo.Abp.Imaging;
+
+public class SkiaSharpCompressOptions
+{
+ public int Quality { get; set; }
+
+ public SkiaSharpCompressOptions()
+ {
+ Quality = 75;
+ }
+}
diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs
new file mode 100644
index 0000000000..c3117d0461
--- /dev/null
+++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs
@@ -0,0 +1,152 @@
+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 SkiaSharpImageCompressorContributor : IImageCompressorContributor, ITransientDependency
+{
+ protected SkiaSharpCompressOptions Options { get; }
+
+ public SkiaSharpImageCompressorContributor(IOptions options)
+ {
+ Options = options.Value;
+ }
+
+ public virtual async Task> TryCompressAsync(
+ Stream stream,
+ string? mimeType = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType))
+ {
+ return new ImageCompressResult(stream, ImageProcessState.Unsupported);
+ }
+
+ var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream, cancellationToken);
+ var originalLength = memoryBitmapStream.Length;
+
+ try
+ {
+ using var codec = SKCodec.Create(memorySkCodecStream);
+ if (codec == null || !CanEncodeFormat(codec.EncodedFormat))
+ {
+ return new ImageCompressResult(stream, ImageProcessState.Unsupported);
+ }
+
+ using var bitmap = SKBitmap.Decode(memoryBitmapStream);
+ if (bitmap == null)
+ {
+ return new ImageCompressResult(stream, ImageProcessState.Unsupported);
+ }
+
+ using var image = SKImage.FromBitmap(bitmap);
+ using var encoded = image.Encode(codec.EncodedFormat, Options.Quality);
+
+ var output = new MemoryStream();
+ try
+ {
+ encoded.SaveTo(output);
+ output.Position = 0;
+
+ if (output.Length < originalLength)
+ {
+ return new ImageCompressResult(output, ImageProcessState.Done);
+ }
+
+ output.Dispose();
+ return new ImageCompressResult(stream, ImageProcessState.Canceled);
+ }
+ catch
+ {
+ output.Dispose();
+ throw;
+ }
+ }
+ finally
+ {
+ memoryBitmapStream.Dispose();
+ memorySkCodecStream.Dispose();
+ }
+ }
+
+ public virtual async Task> TryCompressAsync(
+ byte[] bytes,
+ string? mimeType = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType))
+ {
+ return new ImageCompressResult(bytes, ImageProcessState.Unsupported);
+ }
+
+ using var ms = new MemoryStream(bytes);
+ var result = await TryCompressAsync(ms, mimeType, cancellationToken);
+
+ if (result.State != ImageProcessState.Done)
+ {
+ return new ImageCompressResult(bytes, result.State);
+ }
+
+ var newBytes = await result.Result.GetAllBytesAsync(cancellationToken);
+ result.Result.Dispose();
+ return new ImageCompressResult(newBytes, result.State);
+ }
+
+ protected virtual bool CanCompress(string? mimeType)
+ {
+ return mimeType switch {
+ MimeTypes.Image.Jpeg => true,
+ MimeTypes.Image.Png => true,
+ MimeTypes.Image.Webp => true,
+ _ => false
+ };
+ }
+
+ protected virtual bool CanEncodeFormat(SKEncodedImageFormat format)
+ {
+ return format switch
+ {
+ SKEncodedImageFormat.Jpeg => true,
+ SKEncodedImageFormat.Png => true,
+ SKEncodedImageFormat.Webp => true,
+ _ => false
+ };
+ }
+
+ protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream, CancellationToken cancellationToken)
+ {
+ var streamPosition = stream.CanSeek ? stream.Position : 0;
+
+ var memoryBitmapStream = new MemoryStream();
+ var memorySkCodecStream = new MemoryStream();
+
+ try
+ {
+ await stream.CopyToAsync(memoryBitmapStream, cancellationToken);
+
+ if (stream.CanSeek)
+ {
+ stream.Position = streamPosition;
+ }
+
+ memoryBitmapStream.Position = 0;
+ await memoryBitmapStream.CopyToAsync(memorySkCodecStream, cancellationToken);
+
+ memoryBitmapStream.Position = 0;
+ memorySkCodecStream.Position = 0;
+
+ return (memoryBitmapStream, memorySkCodecStream);
+ }
+ catch
+ {
+ memoryBitmapStream.Dispose();
+ memorySkCodecStream.Dispose();
+ throw;
+ }
+ }
+}
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
index 7a5db23620..dd7da3251a 100644
--- a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs
+++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs
@@ -47,34 +47,76 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans
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((int)resizeArgs.Width, (int)resizeArgs.Height), Options.SKSamplingOptions);
- using var image = SKImage.FromBitmap(resized);
- using var codec = SKCodec.Create(memorySkCodecStream);
- var memoryStream = new MemoryStream();
- using var skData = image.Encode(codec.EncodedFormat, Options.Quality);
- skData.SaveTo(memoryStream);
- return new ImageResizeResult(memoryStream, ImageProcessState.Done);
+ var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream, cancellationToken);
+
+ try
+ {
+ using var codec = SKCodec.Create(memorySkCodecStream);
+ if (codec == null || !CanEncodeFormat(codec.EncodedFormat))
+ {
+ return new ImageResizeResult(stream, ImageProcessState.Unsupported);
+ }
+
+ using var original = SKBitmap.Decode(memoryBitmapStream);
+ if (original == null)
+ {
+ return new ImageResizeResult(stream, ImageProcessState.Unsupported);
+ }
+
+ using var resized = ApplyResize(original, resizeArgs);
+ using var image = SKImage.FromBitmap(resized);
+
+ var memoryStream = new MemoryStream();
+ try
+ {
+ using var skData = image.Encode(codec.EncodedFormat, Options.Quality);
+ skData.SaveTo(memoryStream);
+ memoryStream.Position = 0;
+ return new ImageResizeResult(memoryStream, ImageProcessState.Done);
+ }
+ catch
+ {
+ memoryStream.Dispose();
+ throw;
+ }
+ }
+ finally
+ {
+ memoryBitmapStream.Dispose();
+ memorySkCodecStream.Dispose();
+ }
}
- protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream)
+ protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream, CancellationToken cancellationToken = default)
{
- var streamPosition = stream.Position;
+ var streamPosition = stream.CanSeek ? stream.Position : 0;
var memoryBitmapStream = new MemoryStream();
var memorySkCodecStream = new MemoryStream();
- await stream.CopyToAsync(memoryBitmapStream);
- stream.Position = streamPosition;
- await stream.CopyToAsync(memorySkCodecStream);
- stream.Position = streamPosition;
+ try
+ {
+ await stream.CopyToAsync(memoryBitmapStream, cancellationToken);
+
+ if (stream.CanSeek)
+ {
+ stream.Position = streamPosition;
+ }
+
+ memoryBitmapStream.Position = 0;
+ await memoryBitmapStream.CopyToAsync(memorySkCodecStream, cancellationToken);
- memoryBitmapStream.Position = 0;
- memorySkCodecStream.Position = 0;
+ memoryBitmapStream.Position = 0;
+ memorySkCodecStream.Position = 0;
- return (memoryBitmapStream, memorySkCodecStream);
+ return (memoryBitmapStream, memorySkCodecStream);
+ }
+ catch
+ {
+ memoryBitmapStream.Dispose();
+ memorySkCodecStream.Dispose();
+ throw;
+ }
}
protected virtual bool CanResize(string? mimeType)
@@ -86,4 +128,150 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans
_ => false
};
}
+
+ protected virtual bool CanEncodeFormat(SKEncodedImageFormat format)
+ {
+ return format switch
+ {
+ SKEncodedImageFormat.Jpeg => true,
+ SKEncodedImageFormat.Png => true,
+ SKEncodedImageFormat.Webp => true,
+ _ => false
+ };
+ }
+
+ protected virtual SKBitmap ApplyResize(SKBitmap source, ImageResizeArgs resizeArgs)
+ {
+ var targetWidth = (int)resizeArgs.Width;
+ var targetHeight = (int)resizeArgs.Height;
+
+ if (targetWidth <= 0 && targetHeight <= 0)
+ {
+ return source.Copy();
+ }
+
+ if (targetWidth <= 0)
+ {
+ targetWidth = Math.Max(1, (int)Math.Round((double)source.Width * targetHeight / source.Height));
+ }
+ else if (targetHeight <= 0)
+ {
+ targetHeight = Math.Max(1, (int)Math.Round((double)source.Height * targetWidth / source.Width));
+ }
+
+ var mode = resizeArgs.Mode == ImageResizeMode.Default ? ImageResizeMode.Stretch : resizeArgs.Mode;
+
+ switch (mode)
+ {
+ case ImageResizeMode.None:
+ case ImageResizeMode.Stretch:
+ return source.Resize(new SKImageInfo(targetWidth, targetHeight), Options.SKSamplingOptions);
+
+ case ImageResizeMode.Max:
+ {
+ var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height);
+ var newW = Math.Max(1, (int)Math.Round(source.Width * scale));
+ var newH = Math.Max(1, (int)Math.Round(source.Height * scale));
+ return source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions);
+ }
+
+ case ImageResizeMode.Min:
+ {
+ var scale = Math.Max((double)targetWidth / source.Width, (double)targetHeight / source.Height);
+ var newW = Math.Max(1, (int)Math.Round(source.Width * scale));
+ var newH = Math.Max(1, (int)Math.Round(source.Height * scale));
+ return source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions);
+ }
+
+ case ImageResizeMode.Crop:
+ {
+ var scale = Math.Max((double)targetWidth / source.Width, (double)targetHeight / source.Height);
+ var intermediateW = Math.Max(1, (int)Math.Round(source.Width * scale));
+ var intermediateH = Math.Max(1, (int)Math.Round(source.Height * scale));
+ using var intermediate = source.Resize(new SKImageInfo(intermediateW, intermediateH), Options.SKSamplingOptions);
+
+ var bitmap = new SKBitmap(targetWidth, targetHeight);
+ try
+ {
+ using var canvas = new SKCanvas(bitmap);
+ var srcX = (intermediateW - targetWidth) / 2;
+ var srcY = (intermediateH - targetHeight) / 2;
+ canvas.DrawBitmap(
+ intermediate,
+ new SKRect(srcX, srcY, srcX + targetWidth, srcY + targetHeight),
+ new SKRect(0, 0, targetWidth, targetHeight));
+ return bitmap;
+ }
+ catch
+ {
+ bitmap.Dispose();
+ throw;
+ }
+ }
+
+ case ImageResizeMode.Pad:
+ {
+ var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height);
+ var intermediateW = Math.Max(1, (int)Math.Round(source.Width * scale));
+ var intermediateH = Math.Max(1, (int)Math.Round(source.Height * scale));
+ using var intermediate = source.Resize(new SKImageInfo(intermediateW, intermediateH), Options.SKSamplingOptions);
+
+ var bitmap = new SKBitmap(targetWidth, targetHeight);
+ try
+ {
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Transparent);
+ var dstX = (targetWidth - intermediateW) / 2;
+ var dstY = (targetHeight - intermediateH) / 2;
+ canvas.DrawBitmap(intermediate, new SKPoint(dstX, dstY));
+ return bitmap;
+ }
+ catch
+ {
+ bitmap.Dispose();
+ throw;
+ }
+ }
+
+ case ImageResizeMode.BoxPad:
+ {
+ SKBitmap? scaled = null;
+ try
+ {
+ var working = source;
+ if (source.Width > targetWidth || source.Height > targetHeight)
+ {
+ var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height);
+ var newW = Math.Max(1, (int)Math.Round(source.Width * scale));
+ var newH = Math.Max(1, (int)Math.Round(source.Height * scale));
+ scaled = source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions);
+ working = scaled;
+ }
+
+ var bitmap = new SKBitmap(targetWidth, targetHeight);
+ try
+ {
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Transparent);
+ var dstX = (targetWidth - working.Width) / 2;
+ var dstY = (targetHeight - working.Height) / 2;
+ canvas.DrawBitmap(working, new SKPoint(dstX, dstY));
+ return bitmap;
+ }
+ catch
+ {
+ bitmap.Dispose();
+ throw;
+ }
+ }
+ finally
+ {
+ scaled?.Dispose();
+ }
+ }
+
+ default:
+ throw new NotSupportedException("Resize mode " + resizeArgs.Mode + " is not supported!");
+ }
+ }
}
diff --git a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif
new file mode 100644
index 0000000000..27f6a1e4dc
Binary files /dev/null and b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif differ
diff --git a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs
index de249fb2e5..2696bcbd3b 100644
--- a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs
+++ b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs
@@ -19,7 +19,12 @@ public static class ImageFileHelper
{
return GetTestFileStream("abp.webp");
}
-
+
+ public static Stream GetGifTestFileStream()
+ {
+ return GetTestFileStream("abp.gif");
+ }
+
private static Stream GetTestFileStream(string fileName)
{
var assembly = typeof(ImageFileHelper).Assembly;
diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs
new file mode 100644
index 0000000000..4c575d9f04
--- /dev/null
+++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs
@@ -0,0 +1,179 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Imaging;
+
+public class SkiaSharpImageCompressorTests : AbpImagingSkiaSharpTestBase
+{
+ public IImageCompressor ImageCompressor { get; }
+
+ public SkiaSharpImageCompressorTests()
+ {
+ ImageCompressor = GetRequiredService();
+ }
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ services.Configure(options =>
+ {
+ options.Quality = 50;
+ });
+
+ base.AfterAddApplication(services);
+ }
+
+ [Fact]
+ public async Task Should_Compress_Jpg()
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var compressedImage = await ImageCompressor.CompressAsync(jpegImage);
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBe(ImageProcessState.Done);
+ compressedImage.Result.Length.ShouldBeLessThan(jpegImage.Length);
+ compressedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Compress_Png()
+ {
+ await using var pngImage = ImageFileHelper.GetPngTestFileStream();
+ var compressedImage = await ImageCompressor.CompressAsync(pngImage);
+
+ compressedImage.ShouldNotBeNull();
+
+ if (compressedImage.State == ImageProcessState.Done)
+ {
+ compressedImage.Result.Length.ShouldBeLessThan(pngImage.Length);
+ }
+ else
+ {
+ compressedImage.State.ShouldBe(ImageProcessState.Canceled);
+ compressedImage.Result.Length.ShouldBe(pngImage.Length);
+ }
+
+ compressedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Compress_Webp()
+ {
+ await using var webpImage = ImageFileHelper.GetWebpTestFileStream();
+ var compressedImage = await ImageCompressor.CompressAsync(webpImage);
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBe(ImageProcessState.Done);
+ compressedImage.Result.Length.ShouldBeLessThan(webpImage.Length);
+ compressedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Compress_Stream_And_Byte_Array_The_Same()
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var byteArr = await jpegImage.GetAllBytesAsync();
+
+ var compressedImage1 = await ImageCompressor.CompressAsync(jpegImage);
+ var compressedImage2 = await ImageCompressor.CompressAsync(byteArr);
+
+ compressedImage1.ShouldNotBeNull();
+ compressedImage1.State.ShouldBe(ImageProcessState.Done);
+
+ compressedImage2.ShouldNotBeNull();
+ compressedImage2.State.ShouldBe(ImageProcessState.Done);
+
+ compressedImage1.Result.Length.ShouldBeLessThan(jpegImage.Length);
+ compressedImage2.Result.LongLength.ShouldBeLessThan(jpegImage.Length);
+
+ compressedImage1.Result.Length.ShouldBe(compressedImage2.Result.LongLength);
+
+ compressedImage1.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Return_Compressed_Stream_Positioned_At_Start()
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var compressedImage = await ImageCompressor.CompressAsync(jpegImage);
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBe(ImageProcessState.Done);
+ compressedImage.Result.Position.ShouldBe(0);
+
+ using var copy = new MemoryStream();
+ await compressedImage.Result.CopyToAsync(copy);
+ copy.Length.ShouldBe(compressedImage.Result.Length);
+
+ compressedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Return_Unsupported_For_Gif_Stream()
+ {
+ await using var gifImage = ImageFileHelper.GetGifTestFileStream();
+ var compressedImage = await ImageCompressor.CompressAsync(gifImage);
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBe(ImageProcessState.Unsupported);
+ compressedImage.Result.ShouldBe(gifImage);
+ }
+
+ [Fact]
+ public async Task Should_Return_Unsupported_For_Gif_Bytes()
+ {
+ await using var gifImage = ImageFileHelper.GetGifTestFileStream();
+ var bytes = await gifImage.GetAllBytesAsync();
+ var compressedImage = await ImageCompressor.CompressAsync(bytes);
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBe(ImageProcessState.Unsupported);
+ compressedImage.Result.ShouldBe(bytes);
+ }
+
+ [Fact]
+ public async Task Should_Handle_Non_Seekable_Stream_Directly_On_Contributor()
+ {
+ var contributor = GetRequiredService();
+
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var bytes = await jpegImage.GetAllBytesAsync();
+ await using var nonSeekable = new NonSeekableStream(new MemoryStream(bytes));
+
+ var compressedImage = await contributor.TryCompressAsync(nonSeekable, "image/jpeg");
+
+ compressedImage.ShouldNotBeNull();
+ compressedImage.State.ShouldBeOneOf(ImageProcessState.Done, ImageProcessState.Canceled);
+ if (compressedImage.State == ImageProcessState.Done)
+ {
+ compressedImage.Result.ShouldNotBe(nonSeekable);
+ compressedImage.Result.Dispose();
+ }
+ }
+
+ private sealed class NonSeekableStream : Stream
+ {
+ private readonly Stream _inner;
+ public NonSeekableStream(Stream inner) { _inner = inner; }
+ public override bool CanRead => _inner.CanRead;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override long Length => throw new NotSupportedException();
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+ public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.ReadAsync(buffer, offset, count, cancellationToken);
+ public override void Flush() { }
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing) _inner.Dispose();
+ base.Dispose(disposing);
+ }
+ }
+}
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
index 460c83bf5b..593beb506d 100644
--- 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
@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using Shouldly;
+using SkiaSharp;
using Xunit;
namespace Volo.Abp.Imaging;
@@ -72,4 +73,120 @@ public class SkiaSharpImageResizerTests : AbpImagingSkiaSharpTestBase
resizedImage1.Result.Dispose();
}
+
+ [Fact]
+ public async Task Should_Return_Resized_Stream_Positioned_At_Start()
+ {
+ 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.Position.ShouldBe(0);
+
+ using var copy = new MemoryStream();
+ await resizedImage.Result.CopyToAsync(copy);
+ copy.Length.ShouldBe(resizedImage.Result.Length);
+
+ resizedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Return_Unsupported_For_Gif_Stream()
+ {
+ await using var gifImage = ImageFileHelper.GetGifTestFileStream();
+ var resizedImage = await ImageResizer.ResizeAsync(gifImage, new ImageResizeArgs(100, 100));
+
+ resizedImage.ShouldNotBeNull();
+ resizedImage.State.ShouldBe(ImageProcessState.Unsupported);
+ resizedImage.Result.ShouldBe(gifImage);
+ }
+
+ [Fact]
+ public async Task Should_Return_Unsupported_For_Gif_Bytes()
+ {
+ await using var gifImage = ImageFileHelper.GetGifTestFileStream();
+ var bytes = await gifImage.GetAllBytesAsync();
+ var resizedImage = await ImageResizer.ResizeAsync(bytes, new ImageResizeArgs(100, 100));
+
+ resizedImage.ShouldNotBeNull();
+ resizedImage.State.ShouldBe(ImageProcessState.Unsupported);
+ resizedImage.Result.ShouldBe(bytes);
+ }
+
+ [Theory]
+ [InlineData(ImageResizeMode.None)]
+ [InlineData(ImageResizeMode.Stretch)]
+ [InlineData(ImageResizeMode.Crop)]
+ [InlineData(ImageResizeMode.Pad)]
+ [InlineData(ImageResizeMode.BoxPad)]
+ [InlineData(ImageResizeMode.Default)]
+ public async Task Should_Produce_Exact_Target_Size_For_Fixed_Modes(ImageResizeMode mode)
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, mode));
+
+ resizedImage.State.ShouldBe(ImageProcessState.Done);
+ using var decoded = SKBitmap.Decode(resizedImage.Result);
+ decoded.ShouldNotBeNull();
+ decoded.Width.ShouldBe(120);
+ decoded.Height.ShouldBe(80);
+ resizedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Produce_Bounded_Size_For_Max_Mode()
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, ImageResizeMode.Max));
+
+ resizedImage.State.ShouldBe(ImageProcessState.Done);
+ using var decoded = SKBitmap.Decode(resizedImage.Result);
+ decoded.Width.ShouldBeLessThanOrEqualTo(120);
+ decoded.Height.ShouldBeLessThanOrEqualTo(80);
+ (decoded.Width == 120 || decoded.Height == 80).ShouldBeTrue();
+ resizedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Max_Fit_Source_Larger_Than_Target_For_BoxPad_Mode()
+ {
+ using var src = new SKBitmap(400, 100);
+ using (var canvas = new SKCanvas(src))
+ {
+ canvas.Clear(SKColors.Red);
+ }
+ using var srcImage = SKImage.FromBitmap(src);
+ using var srcData = srcImage.Encode(SKEncodedImageFormat.Png, 100);
+ using var srcStream = new MemoryStream();
+ srcData.SaveTo(srcStream);
+ srcStream.Position = 0;
+
+ var resizedImage = await ImageResizer.ResizeAsync(srcStream, new ImageResizeArgs(100, 100, ImageResizeMode.BoxPad));
+
+ resizedImage.State.ShouldBe(ImageProcessState.Done);
+ using var decoded = SKBitmap.Decode(resizedImage.Result);
+ decoded.Width.ShouldBe(100);
+ decoded.Height.ShouldBe(100);
+
+ decoded.GetPixel(50, 0).Alpha.ShouldBe((byte)0);
+ decoded.GetPixel(50, 99).Alpha.ShouldBe((byte)0);
+ decoded.GetPixel(50, 50).Red.ShouldBeGreaterThan((byte)200);
+
+ resizedImage.Result.Dispose();
+ }
+
+ [Fact]
+ public async Task Should_Produce_Bounded_Size_For_Min_Mode()
+ {
+ await using var jpegImage = ImageFileHelper.GetJpgTestFileStream();
+ var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, ImageResizeMode.Min));
+
+ resizedImage.State.ShouldBe(ImageProcessState.Done);
+ using var decoded = SKBitmap.Decode(resizedImage.Result);
+ decoded.Width.ShouldBeGreaterThanOrEqualTo(120);
+ decoded.Height.ShouldBeGreaterThanOrEqualTo(80);
+ (decoded.Width == 120 || decoded.Height == 80).ShouldBeTrue();
+ resizedImage.Result.Dispose();
+ }
}