Browse Source

Merge pull request #25437 from abpframework/auto-merge/rel-10-4/4574

Merge branch dev with rel-10.4
pull/25011/merge
Volosoft Agent 7 days ago
committed by GitHub
parent
commit
2e1f07135b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Directory.Packages.props
  2. 68
      docs/en/framework/infrastructure/image-manipulation.md
  3. 1
      docs/en/package-version-changes.md
  4. 11
      framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs
  5. 152
      framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs
  6. 226
      framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs
  7. BIN
      framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif
  8. 7
      framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs
  9. 179
      framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs
  10. 117
      framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs

2
Directory.Packages.props

@ -55,7 +55,7 @@
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageVersion Include="LdapForNet" Version="2.7.15" />
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.9.1" />
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.13.0" />
<PackageVersion Include="MailKit" Version="4.13.0" />
<PackageVersion Include="Markdig.Signed" Version="0.42.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />

68
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<ImageSharpCompressOptions>(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<SkiaSharpResizerOptions>(options =>
{
options.Quality = 80;
});
Configure<SkiaSharpCompressOptions>(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.

1
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

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

152
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<SkiaSharpCompressOptions> options)
{
Options = options.Value;
}
public virtual async Task<ImageCompressResult<Stream>> TryCompressAsync(
Stream stream,
string? mimeType = null,
CancellationToken cancellationToken = default)
{
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType))
{
return new ImageCompressResult<Stream>(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>(stream, ImageProcessState.Unsupported);
}
using var bitmap = SKBitmap.Decode(memoryBitmapStream);
if (bitmap == null)
{
return new ImageCompressResult<Stream>(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<Stream>(output, ImageProcessState.Done);
}
output.Dispose();
return new ImageCompressResult<Stream>(stream, ImageProcessState.Canceled);
}
catch
{
output.Dispose();
throw;
}
}
finally
{
memoryBitmapStream.Dispose();
memorySkCodecStream.Dispose();
}
}
public virtual async Task<ImageCompressResult<byte[]>> TryCompressAsync(
byte[] bytes,
string? mimeType = null,
CancellationToken cancellationToken = default)
{
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType))
{
return new ImageCompressResult<byte[]>(bytes, ImageProcessState.Unsupported);
}
using var ms = new MemoryStream(bytes);
var result = await TryCompressAsync(ms, mimeType, cancellationToken);
if (result.State != ImageProcessState.Done)
{
return new ImageCompressResult<byte[]>(bytes, result.State);
}
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken);
result.Result.Dispose();
return new ImageCompressResult<byte[]>(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;
}
}
}

226
framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs

@ -47,34 +47,76 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans
return new ImageResizeResult<Stream>(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<Stream>(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>(stream, ImageProcessState.Unsupported);
}
using var original = SKBitmap.Decode(memoryBitmapStream);
if (original == null)
{
return new ImageResizeResult<Stream>(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<Stream>(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!");
}
}
}

BIN
framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

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

179
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<IImageCompressor>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<SkiaSharpCompressOptions>(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<IImageCompressorContributor>();
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<int> 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);
}
}
}

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

Loading…
Cancel
Save