mirror of https://github.com/abpframework/abp.git
Browse Source
- Add SkiaSharpImageCompressorContributor and SkiaSharpCompressOptions so Volo.Abp.Imaging.SkiaSharp ships a compressor alongside its existing resizer. - Bump Magick.NET-Q16-AnyCPU 14.9.1 -> 14.13.0 to clear the NU1901/NU1902/NU1903 advisories.pull/25427/head
4 changed files with 258 additions and 1 deletions
@ -0,0 +1,11 @@ |
|||||
|
namespace Volo.Abp.Imaging; |
||||
|
|
||||
|
public class SkiaSharpCompressOptions |
||||
|
{ |
||||
|
public int Quality { get; set; } |
||||
|
|
||||
|
public SkiaSharpCompressOptions() |
||||
|
{ |
||||
|
Quality = 75; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
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); |
||||
|
|
||||
|
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 < stream.Length) |
||||
|
{ |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
using System.IO; |
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue