Browse Source

implement cancellation for Jpeg

pull/1574/head
Anton Firszov 6 years ago
parent
commit
9622534276
  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;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
@ -50,6 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private HuffmanScanBuffer scanBuffer; private HuffmanScanBuffer scanBuffer;
private CancellationToken cancellationToken;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class. /// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class.
/// </summary> /// </summary>
@ -63,6 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="spectralEnd">The spectral selection end.</param> /// <param name="spectralEnd">The spectral selection end.</param>
/// <param name="successiveHigh">The successive approximation bit high end.</param> /// <param name="successiveHigh">The successive approximation bit high end.</param>
/// <param name="successiveLow">The successive approximation bit low end.</param> /// <param name="successiveLow">The successive approximation bit low end.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
public HuffmanScanDecoder( public HuffmanScanDecoder(
BufferedReadStream stream, BufferedReadStream stream,
JpegFrame frame, JpegFrame frame,
@ -73,7 +77,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int spectralStart, int spectralStart,
int spectralEnd, int spectralEnd,
int successiveHigh, int successiveHigh,
int successiveLow) int successiveLow,
CancellationToken cancellationToken)
{ {
this.dctZigZag = ZigZag.CreateUnzigTable(); this.dctZigZag = ZigZag.CreateUnzigTable();
this.stream = stream; this.stream = stream;
@ -89,6 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.spectralEnd = spectralEnd; this.spectralEnd = spectralEnd;
this.successiveHigh = successiveHigh; this.successiveHigh = successiveHigh;
this.successiveLow = successiveLow; this.successiveLow = successiveLow;
this.cancellationToken = cancellationToken;
} }
/// <summary> /// <summary>
@ -96,6 +102,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary> /// </summary>
public void ParseEntropyCodedData() public void ParseEntropyCodedData()
{ {
this.cancellationToken.ThrowIfCancellationRequested();
if (!this.frame.Progressive) if (!this.frame.Progressive)
{ {
this.ParseBaselineData(); this.ParseBaselineData();
@ -145,6 +153,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
for (int j = 0; j < mcusPerColumn; j++) for (int j = 0; j < mcusPerColumn; j++)
{ {
this.cancellationToken.ThrowIfCancellationRequested();
for (int i = 0; i < mcusPerLine; i++) for (int i = 0; i < mcusPerLine; i++)
{ {
// Scan an interleaved mcu... process components in order // 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++) for (int j = 0; j < h; j++)
{ {
this.cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j); Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); 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++) for (int j = 0; j < h; j++)
{ {
this.cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j); Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); 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++) for (int j = 0; j < h; j++)
{ {
this.cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j); Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);

5
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Threading;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -111,7 +112,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam> /// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="destination">The destination image</param> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.PixelRowCounter = 0; this.PixelRowCounter = 0;
@ -123,6 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height)
{ {
cancellationToken.ThrowIfCancellationRequested();
this.DoPostProcessorStep(destination); 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) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.ParseStream(stream); this.ParseStream(stream, cancellationToken: cancellationToken);
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
this.InitDerivedMetadataProperties(); this.InitDerivedMetadataProperties();
return this.PostProcessIntoImage<TPixel>(); return this.PostProcessIntoImage<TPixel>(cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ParseStream(stream, true); this.ParseStream(stream, true, cancellationToken);
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
@ -237,7 +237,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <param name="stream">The input stream</param> /// <param name="stream">The input stream</param>
/// <param name="metadataOnly">Whether to decode metadata only.</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(); this.Metadata = new ImageMetadata();
@ -283,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
if (!metadataOnly) if (!metadataOnly)
{ {
this.ProcessStartOfScanMarker(stream); this.ProcessStartOfScanMarker(stream, cancellationToken);
break; break;
} }
else else
@ -990,8 +991,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Processes the SOS (Start of scan marker). /// Processes the SOS (Start of scan marker).
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationToken cancellationToken)
private void ProcessStartOfScanMarker(BufferedReadStream stream)
{ {
if (this.Frame is null) if (this.Frame is null)
{ {
@ -1042,7 +1042,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
spectralStart, spectralStart,
spectralEnd, spectralEnd,
successiveApproximation >> 4, successiveApproximation >> 4,
successiveApproximation & 15); successiveApproximation & 15,
cancellationToken);
sd.ParseEntropyCodedData(); sd.ParseEntropyCodedData();
} }
@ -1075,7 +1076,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
private Image<TPixel> PostProcessIntoImage<TPixel>() private Image<TPixel> PostProcessIntoImage<TPixel>(CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (this.ImageWidth == 0 || this.ImageHeight == 0) if (this.ImageWidth == 0 || this.ImageHeight == 0)
@ -1091,7 +1092,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this)) using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this))
{ {
postProcessor.PostProcess(image.Frames.RootFrame); postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken);
} }
return image; 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> /// <summary>
/// Create a new instance of the <see cref="Image"/> class from the given file. /// Create a new instance of the <see cref="Image"/> class from the given file.
/// </summary> /// </summary>

48
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -4,6 +4,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
@ -103,7 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Progressive.Festzug, 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); provider.LimitAllocatorBufferCapacity().InBytesSqrt(10);
@ -112,6 +114,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException); 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! // DEBUG ONLY!
// The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm"
// into "\tests\Images\ActualOutput\JpegDecoderTests\" // 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 pp = new JpegImagePostProcessor(Configuration.Default, decoder))
using (var image = new Image<Rgba32>(decoder.ImageWidth, decoder.ImageHeight)) using (var image = new Image<Rgba32>(decoder.ImageWidth, decoder.ImageHeight))
{ {
pp.PostProcess(image.Frames.RootFrame); pp.PostProcess(image.Frames.RootFrame, default);
image.DebugSave(provider); image.DebugSave(provider);

12
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -4,8 +4,9 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -164,6 +165,15 @@ namespace SixLabors.ImageSharp.Tests
return cachedImage.Clone(this.Configuration); 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) public override void Deserialize(IXunitSerializationInfo info)
{ {
this.FilePath = info.GetValue<string>("path"); this.FilePath = info.GetValue<string>("path");

6
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Castle.Core.Internal; using Castle.Core.Internal;
using SixLabors.ImageSharp.Formats; 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}!"); 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> /// <summary>
/// Returns an <see cref="Image{TPixel}"/> instance to the test case with the necessary traits. /// Returns an <see cref="Image{TPixel}"/> instance to the test case with the necessary traits.
/// </summary> /// </summary>

Loading…
Cancel
Save