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 index 87912db3fb..c3117d0461 100644 --- a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs @@ -28,6 +28,7 @@ public class SkiaSharpImageCompressorContributor : IImageCompressorContributor, } var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream, cancellationToken); + var originalLength = memoryBitmapStream.Length; try { @@ -52,7 +53,7 @@ public class SkiaSharpImageCompressorContributor : IImageCompressorContributor, encoded.SaveTo(output); output.Position = 0; - if (output.Length < stream.Length) + if (output.Length < originalLength) { return new ImageCompressResult(output, ImageProcessState.Done); } 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 872fa99172..f17ce81fbb 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 @@ -63,7 +63,7 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans return new ImageResizeResult(stream, ImageProcessState.Unsupported); } - using var resized = original.Resize(new SKImageInfo((int)resizeArgs.Width, (int)resizeArgs.Height), Options.SKSamplingOptions); + using var resized = ApplyResize(original, resizeArgs); using var image = SKImage.FromBitmap(resized); var memoryStream = new MemoryStream(); @@ -139,4 +139,142 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans _ => 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.Crop : resizeArgs.Mode; + if (mode == ImageResizeMode.None) + { + mode = ImageResizeMode.Crop; + } + + switch (mode) + { + 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.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs index 0f72658148..4c575d9f04 100644 --- 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 @@ -1,4 +1,6 @@ +using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Shouldly; @@ -132,4 +134,46 @@ public class SkiaSharpImageCompressorTests : AbpImagingSkiaSharpTestBase 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 ecc2c2dd79..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; @@ -112,4 +113,80 @@ public class SkiaSharpImageResizerTests : AbpImagingSkiaSharpTestBase 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(); + } }