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/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..87912db3fb
--- /dev/null
+++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs
@@ -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 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);
+
+ 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 < stream.Length)
+ {
+ 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/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..0963a50fac
--- /dev/null
+++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs
@@ -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();
+ }
+
+ 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();
+ }
+}