From a627488d79f435f5e9553d7f83e45ea898ea6271 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 26 Jul 2020 02:42:43 +0200 Subject: [PATCH] implement JpegEncoder cancellation --- .../Formats/Jpeg/JpegEncoderCore.cs | 18 ++++++++---- .../Formats/Jpg/JpegEncoderTests.cs | 28 +++++++++++++++++++ .../Image/ImageTests.SaveAsync.cs | 20 +++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index acc639eb7..593fe9277 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -201,6 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + cancellationToken.ThrowIfCancellationRequested(); const ushort max = JpegConstants.MaxLength; if (image.Width >= max || image.Height >= max) @@ -249,7 +250,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteDefineHuffmanTables(componentCount); // Write the image data. - this.WriteStartOfScan(image); + this.WriteStartOfScan(image, cancellationToken); // Write the End Of Image marker. this.buffer[0] = JpegConstants.Markers.XFF; @@ -398,7 +399,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void Encode444(Image pixels) + /// The token to monitor for cancellation. + private void Encode444(Image pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -420,6 +422,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int y = 0; y < pixels.Height; y += 8) { + cancellationToken.ThrowIfCancellationRequested(); var currentRows = new RowOctet(pixelBuffer, y); for (int x = 0; x < pixels.Width; x += 8) @@ -945,7 +948,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void WriteStartOfScan(Image image) + /// The token to monitor for cancellation. + private void WriteStartOfScan(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -955,10 +959,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg switch (this.subsample) { case JpegSubsample.Ratio444: - this.Encode444(image); + this.Encode444(image, cancellationToken); break; case JpegSubsample.Ratio420: - this.Encode420(image); + this.Encode420(image, cancellationToken); break; } @@ -972,7 +976,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void Encode420(Image pixels) + /// The token to monitor for cancellation. + private void Encode420(Image pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -1000,6 +1005,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int y = 0; y < pixels.Height; y += 16) { + cancellationToken.ThrowIfCancellationRequested(); for (int x = 0; x < pixels.Width; x += 16) { for (int i = 0; i < 4; i++) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 6c9a74463..11d64e49f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -284,5 +287,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg IccProfile values = input.Metadata.IccProfile; Assert.Equal(values.Entries, actual.Entries); } + + [Theory] + [InlineData(JpegSubsample.Ratio420, 0)] + [InlineData(JpegSubsample.Ratio420, 3)] + [InlineData(JpegSubsample.Ratio420, 10)] + [InlineData(JpegSubsample.Ratio444, 0)] + [InlineData(JpegSubsample.Ratio444, 3)] + [InlineData(JpegSubsample.Ratio444, 10)] + public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs) + { + using var image = new Image(5000, 5000); + using MemoryStream stream = new MemoryStream(); + var cts = new CancellationTokenSource(); + if (cancellationDelayMs == 0) + { + cts.Cancel(); + } + else + { + cts.CancelAfter(cancellationDelayMs); + } + + var encoder = new JpegEncoder() { Subsample = subsample }; + await Assert.ThrowsAnyAsync(() => image.SaveAsync(stream, encoder, cts.Token)); + } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 4a6c96ae8..de819570f 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -3,9 +3,9 @@ using System; using System.IO; - +using System.Threading; using Moq; - +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("test.bmp")] [InlineData("test.jpg")] [InlineData("test.gif")] - public async Task SaveNeverCallsSyncMethods(string filename) + public async Task SaveAsync_NeverCallsSyncMethods(string filename) { using (var image = new Image(5, 5)) { @@ -102,6 +102,20 @@ namespace SixLabors.ImageSharp.Tests } } } + + [Fact] + public async Task SaveAsync_WithNonSeekableStream_IsCancellable() + { + using var image = new Image(500, 500); + IImageEncoder encoder = new BmpEncoder(); + using var stream = new MemoryStream(); + var asyncStream = new AsyncStreamWrapper(stream, () => false); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromTicks(1)); + + await Assert.ThrowsAnyAsync(() => + image.SaveAsync(asyncStream, encoder, cts.Token)); + } } } }