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.
///