Browse Source

implement cancellation for Jpeg

pull/1296/head
Anton Firszov 6 years ago
parent
commit
2a22e866c5
  1. 17
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  2. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs
  3. 21
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  4. 14
      src/ImageSharp/Image.FromFile.cs
  5. 48
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  6. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs
  7. 12
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  8. 6
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

17
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;
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class.
/// </summary>
@ -63,6 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="spectralEnd">The spectral selection end.</param>
/// <param name="successiveHigh">The successive approximation bit high end.</param>
/// <param name="successiveLow">The successive approximation bit low end.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
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;
}
/// <summary>
@ -96,6 +102,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
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<Block8x8> 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<Block8x8> 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<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);

5
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
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="destination">The destination image</param>
public void PostProcess<TPixel>(ImageFrame<TPixel> destination)
/// <param name="cancellationToken">The token to request cancellation.</param>
public void PostProcess<TPixel>(ImageFrame<TPixel> destination, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}

21
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -212,18 +212,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ParseStream(stream);
this.ParseStream(stream, cancellationToken: cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitDerivedMetadataProperties();
return this.PostProcessIntoImage<TPixel>();
return this.PostProcessIntoImage<TPixel>(cancellationToken);
}
/// <inheritdoc/>
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
/// </summary>
/// <param name="stream">The input stream</param>
/// <param name="metadataOnly">Whether to decode metadata only.</param>
public void ParseStream(BufferedReadStream stream, bool metadataOnly = false)
/// <param name="cancellationToken">The token to monitor cancellation.</param>
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
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
/// <param name="stream">The input stream.</param>
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
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
private Image<TPixel> PostProcessIntoImage<TPixel>()
private Image<TPixel> PostProcessIntoImage<TPixel>(CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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;

14
src/ImageSharp/Image.FromFile.cs

@ -301,6 +301,20 @@ namespace SixLabors.ImageSharp
}
}
/// <summary>
/// Create a new instance of the <see cref="Image"/> class from the given file.
/// </summary>
/// <param name="path">The file path to the image.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="ArgumentNullException">The decoder is null.</exception>
/// <exception cref="UnknownImageFormatException">Image format not recognised.</exception>
/// <exception cref="InvalidImageContentException">Image contains invalid content.</exception>
/// <returns>A <see cref="Task{Image}"/> representing the asynchronous operation.</returns>
public static Task<Image> LoadAsync(string path, CancellationToken cancellationToken)
=> LoadAsync(Configuration.Default, path, cancellationToken);
/// <summary>
/// Create a new instance of the <see cref="Image"/> class from the given file.
/// </summary>

48
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<TPixel>(TestImageProvider<TPixel> provider)
public void Decode_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.LimitAllocatorBufferCapacity().InBytesSqrt(10);
@ -112,6 +114,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException);
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)]
public async Task DecodeAsnc_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.LimitAllocatorBufferCapacity().InBytesSqrt(10);
InvalidImageContentException ex = await Assert.ThrowsAsync<InvalidImageContentException>(() => provider.GetImageAsync(JpegDecoder));
this.Output.WriteLine(ex.Message);
Assert.IsType<InvalidMemoryOperationException>(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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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\"

2
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<Rgba32>(decoder.ImageWidth, decoder.ImageHeight))
{
pp.PostProcess(image.Frames.RootFrame);
pp.PostProcess(image.Frames.RootFrame, default);
image.DebugSave(provider);

12
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<Image<TPixel>> 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<TPixel>(this.Configuration, path, decoder);
}
public override void Deserialize(IXunitSerializationInfo info)
{
this.FilePath = info.GetValue<string>("path");

6
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<Image<TPixel>> GetImageAsync(IImageDecoder decoder)
{
throw new NotSupportedException($"Decoder specific GetImageAsync() is not supported with {this.GetType().Name}!");
}
/// <summary>
/// Returns an <see cref="Image{TPixel}"/> instance to the test case with the necessary traits.
/// </summary>

Loading…
Cancel
Save