diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index d6c16f826..6424ee23a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder @@ -50,6 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private HuffmanScanBuffer scanBuffer; + private CancellationToken cancellationToken; + /// /// Initializes a new instance of the class. /// @@ -63,6 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The spectral selection end. /// The successive approximation bit high end. /// The successive approximation bit low end. + /// The token to monitor cancellation. public HuffmanScanDecoder( BufferedReadStream stream, JpegFrame frame, @@ -73,7 +77,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int spectralStart, int spectralEnd, int successiveHigh, - int successiveLow) + int successiveLow, + CancellationToken cancellationToken) { this.dctZigZag = ZigZag.CreateUnzigTable(); this.stream = stream; @@ -89,6 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.spectralEnd = spectralEnd; this.successiveHigh = successiveHigh; this.successiveLow = successiveLow; + this.cancellationToken = cancellationToken; } /// @@ -96,6 +102,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// public void ParseEntropyCodedData() { + this.cancellationToken.ThrowIfCancellationRequested(); + if (!this.frame.Progressive) { this.ParseBaselineData(); @@ -145,6 +153,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < mcusPerColumn; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + for (int i = 0; i < mcusPerLine; i++) { // Scan an interleaved mcu... process components in order @@ -210,6 +220,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); @@ -376,6 +387,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); @@ -402,6 +415,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs index 716bb9eb0..5b0331c85 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Numerics; +using System.Threading; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -111,7 +112,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// The pixel type /// The destination image - public void PostProcess(ImageFrame destination) + /// The token to request cancellation. + public void PostProcess(ImageFrame destination, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { this.PixelRowCounter = 0; @@ -123,6 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) { + cancellationToken.ThrowIfCancellationRequested(); this.DoPostProcessorStep(destination); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 6874d09e9..70f3a9202 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -212,18 +212,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - this.ParseStream(stream); + this.ParseStream(stream, cancellationToken: cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitDerivedMetadataProperties(); - return this.PostProcessIntoImage(); + return this.PostProcessIntoImage(cancellationToken); } /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ParseStream(stream, true); + this.ParseStream(stream, true, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -237,7 +237,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The input stream /// Whether to decode metadata only. - public void ParseStream(BufferedReadStream stream, bool metadataOnly = false) + /// The token to monitor cancellation. + public void ParseStream(BufferedReadStream stream, bool metadataOnly = false, CancellationToken cancellationToken = default) { this.Metadata = new ImageMetadata(); @@ -283,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream); + this.ProcessStartOfScanMarker(stream, cancellationToken); break; } else @@ -990,8 +991,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the SOS (Start of scan marker). /// - /// The input stream. - private void ProcessStartOfScanMarker(BufferedReadStream stream) + private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationToken cancellationToken) { if (this.Frame is null) { @@ -1042,7 +1042,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg spectralStart, spectralEnd, successiveApproximation >> 4, - successiveApproximation & 15); + successiveApproximation & 15, + cancellationToken); sd.ParseEntropyCodedData(); } @@ -1075,7 +1076,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The . - private Image PostProcessIntoImage() + private Image PostProcessIntoImage(CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (this.ImageWidth == 0 || this.ImageHeight == 0) @@ -1091,7 +1092,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this)) { - postProcessor.PostProcess(image.Frames.RootFrame); + postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken); } return image; diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index 1058dd19c..d3464d20d 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -301,6 +301,20 @@ namespace SixLabors.ImageSharp } } + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(string path, CancellationToken cancellationToken) + => LoadAsync(Configuration.Default, path, cancellationToken); + /// /// Create a new instance of the class from the given file. /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 912f606b2..2bc64789e 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.IO; @@ -103,7 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)] - public void DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + public void Decode_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) where TPixel : unmanaged, IPixel { provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); @@ -112,6 +114,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.IsType(ex.InnerException); } + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)] + public async Task DecodeAsnc_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); + InvalidImageContentException ex = await Assert.ThrowsAsync(() => provider.GetImageAsync(JpegDecoder)); + this.Output.WriteLine(ex.Message); + Assert.IsType(ex.InnerException); + } + + [Theory] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 3)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 3)] + public async Task Decode_IsCancellable(string fileName, int waitMilliseconds) + { + string hugeFile = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(waitMilliseconds); + await Assert.ThrowsAsync(() => Image.LoadAsync(hugeFile, cts.Token)); + } + + [Theory] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 3)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 3)] + public async Task Identify_IsCancellable(string fileName, int waitMilliseconds) + { + string hugeFile = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(waitMilliseconds); + await Assert.ThrowsAsync(() => Image.IdentifyAsync(hugeFile, cts.Token)); + } + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs index 12e1ec22b..0dd2abcc1 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) using (var image = new Image(decoder.ImageWidth, decoder.ImageHeight)) { - pp.PostProcess(image.Frames.RootFrame); + pp.PostProcess(image.Frames.RootFrame, default); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 78567f926..440baaa63 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Reflection; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -164,6 +165,15 @@ namespace SixLabors.ImageSharp.Tests return cachedImage.Clone(this.Configuration); } + public override Task> GetImageAsync(IImageDecoder decoder) + { + Guard.NotNull(decoder, nameof(decoder)); + + // Used in small subset of decoder tests, no caching. + string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.FilePath); + return Image.LoadAsync(this.Configuration, path, decoder); + } + public override void Deserialize(IXunitSerializationInfo info) { this.FilePath = info.GetValue("path"); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index da641a296..700c40b72 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using System.Threading.Tasks; using Castle.Core.Internal; using SixLabors.ImageSharp.Formats; @@ -97,6 +98,11 @@ namespace SixLabors.ImageSharp.Tests throw new NotSupportedException($"Decoder specific GetImage() is not supported with {this.GetType().Name}!"); } + public virtual Task> GetImageAsync(IImageDecoder decoder) + { + throw new NotSupportedException($"Decoder specific GetImageAsync() is not supported with {this.GetType().Name}!"); + } + /// /// Returns an instance to the test case with the necessary traits. ///