diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index 5791c6e92b..c5abbda61b 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -73,9 +74,10 @@ public static void AcceptVisitor(this Image source, IImageVisitor visitor) /// /// The source image. /// The image visitor. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor) - => source.AcceptAsync(visitor); + public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) + => source.AcceptAsync(visitor, cancellationToken); /// /// Gets the configuration for the image. diff --git a/src/ImageSharp/Advanced/IImageVisitor.cs b/src/ImageSharp/Advanced/IImageVisitor.cs index fbfdafeb10..ccff180266 100644 --- a/src/ImageSharp/Advanced/IImageVisitor.cs +++ b/src/ImageSharp/Advanced/IImageVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -31,9 +32,10 @@ public interface IImageVisitorAsync /// Provides a pixel-specific implementation for a given operation. /// /// The image. + /// The token to monitor for cancellation requests. /// The pixel type. /// A representing the asynchronous operation. - Task VisitAsync(Image image) + Task VisitAsync(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs index 3b86415324..e4713e237c 100644 --- a/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs +++ b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp { @@ -32,5 +33,10 @@ public InvalidImageContentException(string errorMessage, Exception innerExceptio : base(errorMessage, innerException) { } + + internal InvalidImageContentException(Size size, InvalidMemoryOperationException memoryException) + : this($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {size.Width}x{size.Height}.", memoryException) + { + } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index cb26ff606a..129b3a1aa0 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -36,18 +37,7 @@ public Image Decode(Configuration configuration, Stream stream) Guard.NotNull(stream, nameof(stream)); var decoder = new BmpDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); - } + return decoder.Decode(configuration, stream); } /// @@ -55,46 +45,34 @@ public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new BmpDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new BmpDecoderCore(configuration, this).Identify(bufferedStream); + return new BmpDecoderCore(configuration, this).Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new BmpDecoderCore(configuration, this).IdentifyAsync(bufferedStream); + return new BmpDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index ea8fd11a86..6f92236372 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -118,7 +119,7 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options) public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { try @@ -197,7 +198,7 @@ public Image Decode(BufferedReadStream stream) } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata); diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 08c9bde00d..2f5c4b7cf7 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -42,11 +43,11 @@ public void Encode(Image image, Stream stream) } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index b3f64eea6a..eb29c44050 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; @@ -19,7 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Image encoder for writing an image to a stream as a Windows bitmap. /// - internal sealed class BmpEncoderCore + internal sealed class BmpEncoderCore : IImageEncoderInternals { /// /// The amount to pad each row by. @@ -97,32 +98,9 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 2b7103072b..196d77ad77 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -30,21 +31,7 @@ public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -52,30 +39,17 @@ public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) @@ -85,18 +59,16 @@ public IImageInfo Identify(Configuration configuration, Stream stream) var decoder = new GifDecoderCore(configuration, this); using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Identify(bufferedStream); + return decoder.Identify(bufferedStream, default); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); var decoder = new GifDecoderCore(configuration, this); - - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.IdentifyAsync(bufferedStream); + return decoder.IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 78ffee8bdb..8f5cc3b5c8 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -97,7 +98,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator; /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Image image = null; @@ -158,7 +159,7 @@ public Image Decode(BufferedReadStream stream) } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { try { diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 539ab0fb38..116ee3daeb 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -41,11 +42,11 @@ public void Encode(Image image, Stream stream) } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new GifEncoderCore(image.GetConfiguration(), this); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 556ace203a..070864e603 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -6,6 +6,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Implements the GIF encoding protocol. /// - internal sealed class GifEncoderCore + internal sealed class GifEncoderCore : IImageEncoderInternals { /// /// Used for allocating memory during processing operations. @@ -75,25 +76,9 @@ public GifEncoderCore(Configuration configuration, IGifEncoderOptions options) /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Gif/ImageExtensions.cs b/src/ImageSharp/Formats/Gif/ImageExtensions.cs deleted file mode 100644 index d262b056cd..0000000000 --- a/src/ImageSharp/Formats/Gif/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Gif; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsGif(this Image source, Stream stream) => SaveAsGif(source, stream, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, Stream stream) => SaveAsGifAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index 97886e5269..b55f1119b3 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -38,9 +39,10 @@ Image Decode(Configuration configuration, Stream stream) /// The pixel format. /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task> DecodeAsync(Configuration configuration, Stream stream) + Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// @@ -48,8 +50,9 @@ Task> DecodeAsync(Configuration configuration, Stream stre /// /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task DecodeAsync(Configuration configuration, Stream stream); + Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs index 33748bf245..e190f7adda 100644 --- a/src/ImageSharp/Formats/IImageDecoderInternals.cs +++ b/src/ImageSharp/Formats/IImageDecoderInternals.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; @@ -17,21 +18,36 @@ internal interface IImageDecoderInternals /// Configuration Configuration { get; } + /// + /// Gets the dimensions of the image being decoded. + /// + Size Dimensions { get; } + /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The stream, where the image should be decoded from. Cannot be null. + /// The token to monitor for cancellation requests. /// is null. /// The decoded image. - Image Decode(BufferedReadStream stream) + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// /// Reads the raw image information from the specified stream. /// /// The containing image data. + /// The token to monitor for cancellation requests. /// The . - IImageInfo Identify(BufferedReadStream stream); + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/IImageEncoder.cs b/src/ImageSharp/Formats/IImageEncoder.cs index 646e0ecc09..e5a1b1c839 100644 --- a/src/ImageSharp/Formats/IImageEncoder.cs +++ b/src/ImageSharp/Formats/IImageEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -27,8 +28,9 @@ void Encode(Image image, Stream stream) /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - Task EncodeAsync(Image image, Stream stream) + Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Formats/IImageEncoderInternals.cs b/src/ImageSharp/Formats/IImageEncoderInternals.cs new file mode 100644 index 0000000000..d44ac45f27 --- /dev/null +++ b/src/ImageSharp/Formats/IImageEncoderInternals.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats +{ + /// + /// Abstraction for shared internals for ***DecoderCore implementations to be used with . + /// + internal interface IImageEncoderInternals + { + /// + /// Encodes the image. + /// + /// The image. + /// The stream. + /// The token to monitor for cancellation requests. + /// The pixel type. + void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + } +} diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 862c64999b..6f5fc23338 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; namespace SixLabors.ImageSharp.Formats @@ -24,7 +25,8 @@ public interface IImageInfoDetector /// /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The object - Task IdentifyAsync(Configuration configuration, Stream stream); + Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 9d1639a090..5d77fb0c8c 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -14,21 +17,159 @@ internal static class ImageDecoderUtilities /// Reads the raw image information from the specified stream. /// /// The decoder. - /// The containing image data. + /// /// The configuration for the image. + /// The containing image data. + /// The token to monitor for cancellation requests. /// is null. /// A representing the asynchronous operation. - public static Task IdentifyAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) - => Task.FromResult(decoder.Identify(stream)); + public static Task IdentifyAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + => decoder.IdentifyAsync(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); + + /// + /// Reads the raw image information from the specified stream. + /// + /// The decoder. + /// The configuration for the image. + /// The containing image data. + /// Factory method to handle as . + /// The token to monitor for cancellation requests. + /// is null. + /// A representing the asynchronous operation. + public static Task IdentifyAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func tooLargeImageExceptionFactory, + CancellationToken cancellationToken) + { + try + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + IImageInfo imageInfo = decoder.Identify(bufferedReadStream, cancellationToken); + return Task.FromResult(imageInfo); + } + catch (InvalidMemoryOperationException ex) + { + InvalidImageContentException invalidImageContentException = tooLargeImageExceptionFactory(ex, decoder.Dimensions); + return Task.FromException(invalidImageContentException); + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The decoder. + /// The configuration for the image. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + public static Task> DecodeAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => + decoder.DecodeAsync( + configuration, + stream, + DefaultLargeImageExceptionFactory, + cancellationToken); /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The decoder. - /// The containing image data. + /// The configuration for the image. + /// The containing image data. + /// Factory method to handle as . + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public static Task> DecodeAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) + public static Task> DecodeAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func largeImageExceptionFactory, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - => Task.FromResult(decoder.Decode(stream)); + { + try + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + Image image = decoder.Decode(bufferedReadStream, cancellationToken); + return Task.FromResult(image); + } + catch (InvalidMemoryOperationException ex) + { + InvalidImageContentException invalidImageContentException = largeImageExceptionFactory(ex, decoder.Dimensions); + return Task.FromException>(invalidImageContentException); + } + catch (OperationCanceledException) + { + return Task.FromCanceled>(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException>(ex); + } + } + + public static IImageInfo Identify( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream) + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + + try + { + return decoder.Identify(bufferedReadStream, default); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(decoder.Dimensions, ex); + } + } + + public static Image Decode(this IImageDecoderInternals decoder, Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory); + + public static Image Decode( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func largeImageExceptionFactory) + where TPixel : unmanaged, IPixel + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + + try + { + return decoder.Decode(bufferedReadStream, default); + } + catch (InvalidMemoryOperationException ex) + { + throw largeImageExceptionFactory(ex, decoder.Dimensions); + } + } + + private static InvalidImageContentException DefaultLargeImageExceptionFactory( + InvalidMemoryOperationException memoryOperationException, + Size dimensions) => + new InvalidImageContentException(dimensions, memoryOperationException); } } diff --git a/src/ImageSharp/Formats/ImageEncoderUtilities.cs b/src/ImageSharp/Formats/ImageEncoderUtilities.cs new file mode 100644 index 0000000000..896fffa6fc --- /dev/null +++ b/src/ImageSharp/Formats/ImageEncoderUtilities.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats +{ + internal static class ImageEncoderUtilities + { + public static async Task EncodeAsync( + this IImageEncoderInternals encoder, + Image image, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Configuration configuration = image.GetConfiguration(); + if (stream.CanSeek) + { + await DoEncodeAsync(stream).ConfigureAwait(false); + } + else + { + using var ms = new MemoryStream(); + await DoEncodeAsync(ms); + ms.Position = 0; + await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) + .ConfigureAwait(false); + } + + Task DoEncodeAsync(Stream innerStream) + { + try + { + encoder.Encode(image, innerStream, cancellationToken); + return Task.CompletedTask; + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + } + + public static void Encode( + this IImageEncoderInternals encoder, + Image image, + Stream stream) + where TPixel : unmanaged, IPixel + => encoder.Encode(image, stream, default); + } +} diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs new file mode 100644 index 0000000000..075c708b6a --- /dev/null +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -0,0 +1,539 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; + +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsBmpAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsBmp(this Image source, Stream stream) + => SaveAsBmp(source, stream, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsBmpAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsGifAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsGif(this Image source, Stream stream) + => SaveAsGif(source, stream, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsGifAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsJpegAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsJpeg(this Image source, Stream stream) + => SaveAsJpeg(source, stream, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsJpegAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsPngAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsPng(this Image source, Stream stream) + => SaveAsPng(source, stream, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsPngAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsTgaAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsTga(this Image source, Stream stream) + => SaveAsTga(source, stream, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsTgaAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), + cancellationToken); + + } +} diff --git a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs b/src/ImageSharp/Formats/ImageExtensions.Save.tt similarity index 51% rename from src/ImageSharp/Formats/Bmp/ImageExtensions.cs rename to src/ImageSharp/Formats/ImageExtensions.Save.tt index 8d97c8b464..63b404cc44 100644 --- a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -1,10 +1,32 @@ +<#@ template language="C#" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +// using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Bmp; + +<# + var formats = new []{ + "Bmp", + "Gif", + "Jpeg", + "Png", + "Tga", + }; + + foreach (string fmt in formats) + { +#> +using SixLabors.ImageSharp.Formats.<#= fmt #>; +<# + + } +#> namespace SixLabors.ImageSharp { @@ -13,88 +35,115 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { +<# + foreach (string fmt in formats) + { +#> /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// Thrown if the path is null. - public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null); + public static void SaveAs<#= fmt #>(this Image source, string path) => SaveAs<#= fmt #>(source, path, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null); + public static Task SaveAs<#= fmt #>Async(this Image source, string path) => SaveAs<#= fmt #>Async(source, path, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, string path, CancellationToken cancellationToken) + => SaveAs<#= fmt #>Async(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// The encoder to save the image with. /// Thrown if the path is null. - public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder) => + public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance), + cancellationToken); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// Thrown if the stream is null. - public static void SaveAsBmp(this Image source, Stream stream) => SaveAsBmp(source, stream, null); + public static void SaveAs<#= fmt #>(this Image source, Stream stream) + => SaveAs<#= fmt #>(source, stream, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. + /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, Stream stream) => SaveAsBmpAsync(source, stream, null); + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAs<#= fmt #>Async(source, stream, null, cancellationToken); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) => - source.Save( + /// A representing the asynchronous operation. + public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) + => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder) => + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance), + cancellationToken); + +<# + } +#> } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index d6c16f8260..6424ee23ac 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 @@ internal class HuffmanScanDecoder private HuffmanScanBuffer scanBuffer; + private CancellationToken cancellationToken; + /// /// Initializes a new instance of the class. /// @@ -63,6 +66,7 @@ internal class HuffmanScanDecoder /// 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 @@ public HuffmanScanDecoder( int spectralStart, int spectralEnd, int successiveHigh, - int successiveLow) + int successiveLow, + CancellationToken cancellationToken) { this.dctZigZag = ZigZag.CreateUnzigTable(); this.stream = stream; @@ -89,6 +94,7 @@ public HuffmanScanDecoder( this.spectralEnd = spectralEnd; this.successiveHigh = successiveHigh; this.successiveLow = successiveLow; + this.cancellationToken = cancellationToken; } /// @@ -96,6 +102,8 @@ public HuffmanScanDecoder( /// public void ParseEntropyCodedData() { + this.cancellationToken.ThrowIfCancellationRequested(); + if (!this.frame.Progressive) { this.ParseBaselineData(); @@ -145,6 +153,8 @@ private void ParseBaselineDataInterleaved() 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 @@ private void ParseBaselineDataNonInterleaved() 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 @@ private void ParseProgressiveDataNonInterleaved() 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 @@ ref Unsafe.Add(ref blockRef, i), 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 716bb9eb02..5b0331c85c 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 @@ public void Dispose() /// /// 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 @@ public void PostProcess(ImageFrame destination) while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) { + cancellationToken.ThrowIfCancellationRequested(); this.DoPostProcessorStep(destination); } } diff --git a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs deleted file mode 100644 index d6600b6253..0000000000 --- a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Jpeg; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsJpeg(this Image source, Stream stream) => SaveAsJpeg(source, stream, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, Stream stream) => SaveAsJpegAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 3eaf3a4c47..39b8e492f8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -24,20 +25,7 @@ public Image Decode(Configuration configuration, Stream stream) Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - - JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -45,31 +33,19 @@ public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - - JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) @@ -77,20 +53,22 @@ public IImageInfo Identify(Configuration configuration, Stream stream) Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - - return decoder.Identify(bufferedStream); + return decoder.Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - - return decoder.IdentifyAsync(bufferedStream); + // The introduction of a local variable that refers to an object the implements + // IDisposable means you must use async/await, where the compiler generates the + // state machine and a continuation. + using (var decoder = new JpegDecoderCore(configuration, this)) + { + return await decoder.IdentifyAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + } } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index c0b09c4c2b..c4355cdbe1 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -117,6 +118,9 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) /// public Size ImageSizeInPixels { get; private set; } + /// + Size IImageDecoderInternals.Dimensions => this.ImageSizeInPixels; + /// /// Gets the number of MCU blocks in the image as . /// @@ -205,21 +209,21 @@ public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStrea } /// - public Image Decode(BufferedReadStream stream) + 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) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ParseStream(stream, true); + this.ParseStream(stream, true, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -233,7 +237,8 @@ public IImageInfo Identify(BufferedReadStream stream) /// /// 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(); @@ -263,6 +268,8 @@ public void ParseStream(BufferedReadStream stream, bool metadataOnly = false) while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) { + cancellationToken.ThrowIfCancellationRequested(); + if (!fileMarker.Invalid) { // Get the marker length @@ -279,7 +286,7 @@ public void ParseStream(BufferedReadStream stream, bool metadataOnly = false) case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream); + this.ProcessStartOfScanMarker(stream, cancellationToken); break; } else @@ -996,8 +1003,7 @@ private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int r /// /// 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) { @@ -1048,7 +1054,8 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream) spectralStart, spectralEnd, successiveApproximation >> 4, - successiveApproximation & 15); + successiveApproximation & 15, + cancellationToken); sd.ParseEntropyCodedData(); } @@ -1081,7 +1088,7 @@ private ushort ReadUint16(BufferedReadStream stream) /// /// The pixel format. /// The . - private Image PostProcessIntoImage() + private Image PostProcessIntoImage(CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (this.ImageWidth == 0 || this.ImageHeight == 0) @@ -1097,7 +1104,7 @@ private Image PostProcessIntoImage() 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/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 08d793401e..b549bd8a32 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -43,26 +44,13 @@ public void Encode(Image image, Stream stream) /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - - if (stream.CanSeek) - { - encoder.Encode(image, stream); - } - else - { - // this hack has to be be here because JpegEncoderCore is unsafe - using (var ms = new MemoryStream()) - { - encoder.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index f755028dbd..593fe92776 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -23,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Image encoder for writing an image to a stream as a jpeg. /// - internal sealed unsafe class JpegEncoderCore + internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { /// /// The number of quantization tables. @@ -194,11 +195,13 @@ public JpegEncoderCore(IJpegEncoderOptions options) /// The pixel format. /// The image to write from. /// The stream to write to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + cancellationToken.ThrowIfCancellationRequested(); const ushort max = JpegConstants.MaxLength; if (image.Width >= max || image.Height >= max) @@ -247,7 +250,7 @@ public void Encode(Image image, Stream stream) 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; @@ -396,7 +399,8 @@ private void EmitHuffRLE(HuffIndex index, int runLength, int value) /// /// 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.) @@ -418,6 +422,7 @@ private void Encode444(Image pixels) 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) @@ -943,7 +948,8 @@ private void WriteStartOfFrame(int width, int height, int componentCount) /// /// 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.) @@ -953,10 +959,10 @@ private void WriteStartOfScan(Image image) 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; } @@ -970,7 +976,8 @@ private void WriteStartOfScan(Image image) /// /// 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.) @@ -998,6 +1005,7 @@ private void Encode420(Image pixels) 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/src/ImageSharp/Formats/Png/ImageExtensions.cs b/src/ImageSharp/Formats/Png/ImageExtensions.cs deleted file mode 100644 index e6a5265b27..0000000000 --- a/src/ImageSharp/Formats/Png/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Png; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsPng(this Image source, Stream stream) => SaveAsPng(source, stream, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, Stream stream) => SaveAsPngAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 87e0195c35..479c24b7e8 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -2,9 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -22,65 +21,37 @@ public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { var decoder = new PngDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var decoder = new PngDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Identify(bufferedStream); + return decoder.Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.IdentifyAsync(bufferedStream); + return decoder.IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 89caac3f68..3fa0e3f586 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -9,6 +9,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; @@ -131,7 +133,7 @@ public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) public Size Dimensions => new Size(this.header.Width, this.header.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var metadata = new ImageMetadata(); @@ -223,7 +225,7 @@ public Image Decode(BufferedReadStream stream) } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 61ea7c4684..e72e8d3d55 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -71,13 +72,17 @@ public void Encode(Image image, Stream stream) /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream) + public async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + // The introduction of a local variable that refers to an object the implements + // IDisposable means you must use async/await, where the compiler generates the + // state machine and a continuation. using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) { - await encoder.EncodeAsync(image, stream).ConfigureAwait(false); + await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a39fdd91f7..5cf11099cd 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -21,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Performs the png encoding operation. /// - internal sealed class PngEncoderCore : IDisposable + internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { /// /// The maximum block size, defaults at 64k for uncompressed blocks. @@ -127,31 +128,8 @@ public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configurati /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); diff --git a/src/ImageSharp/Formats/Tga/ImageExtensions.cs b/src/ImageSharp/Formats/Tga/ImageExtensions.cs deleted file mode 100644 index f39738eae3..0000000000 --- a/src/ImageSharp/Formats/Tga/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Tga; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsTga(this Image source, Stream stream) => SaveAsTga(source, stream, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, Stream stream) => SaveAsTgaAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 25aa233db8..e06a0ee887 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -21,21 +22,7 @@ public Image Decode(Configuration configuration, Stream stream) Guard.NotNull(stream, nameof(stream)); var decoder = new TgaDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -43,49 +30,34 @@ public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new TgaDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new TgaDecoderCore(configuration, this).Identify(bufferedStream); + return new TgaDecoderCore(configuration, this).Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new TgaDecoderCore(configuration, this).IdentifyAsync(bufferedStream); + return new TgaDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 7cd83fedbf..eef6e7362b 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -77,7 +78,7 @@ public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { try @@ -640,7 +641,7 @@ private void ReadRle(int width, int height, Buffer2D pixels, int } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadFileHeader(stream); return new ImageInfo( diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index c8f8fb1a51..529d951cd3 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -32,11 +33,11 @@ public void Encode(Image image, Stream stream) } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index d9e7c0e6bc..d3a628531e 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -16,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Image encoder for writing an image to a stream as a truevision targa image. /// - internal sealed class TgaEncoderCore + internal sealed class TgaEncoderCore : IImageEncoderInternals { /// /// Used for allocating memory during processing operations. @@ -61,31 +62,8 @@ public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocato /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 683590fd1a..da23fb47dd 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -156,18 +157,24 @@ private static (Image Image, IImageFormat Format) Decode(Stream /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// The pixel format. /// A representing the asynchronous operation. - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync( + Stream stream, + Configuration config, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); + (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config) + .ConfigureAwait(false); if (decoder is null) { return (null, null); } - Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + Image img = await decoder.DecodeAsync(config, stream, cancellationToken) + .ConfigureAwait(false); return (img, format); } @@ -183,7 +190,7 @@ private static (Image Image, IImageFormat Format) Decode(Stream stream, Configur return (img, format); } - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config, CancellationToken cancellationToken) { (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); if (decoder is null) @@ -191,7 +198,7 @@ private static (Image Image, IImageFormat Format) Decode(Stream stream, Configur return (null, null); } - Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + Image img = await decoder.DecodeAsync(config, stream, cancellationToken).ConfigureAwait(false); return (img, format); } @@ -221,11 +228,12 @@ private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(Stre /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// /// A representing the asynchronous operation with the /// property of the returned type set to null if a suitable detector /// is not found. - private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentityAsync(Stream stream, Configuration config) + private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentityAsync(Stream stream, Configuration config, CancellationToken cancellationToken) { (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); @@ -239,7 +247,7 @@ private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(Stre return (null, format); } - IImageInfo info = await detector.IdentifyAsync(config, stream).ConfigureAwait(false); + IImageInfo info = await detector.IdentifyAsync(config, stream, cancellationToken).ConfigureAwait(false); return (info, format); } } diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index a078f2db98..bf239c3e9f 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -80,15 +81,75 @@ public static IImageInfo Identify(Configuration configuration, string filePath, } /// - /// Create a new instance of the class from the given file. + /// Reads the raw image information from the specified stream without fully decoding it. /// - /// The file path to the image. - /// - /// Thrown if the stream is not readable nor seekable. - /// - /// The . - public static Image Load(string path) - => Load(Configuration.Default, path); + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static Task IdentifyAsync(string filePath, CancellationToken cancellationToken = default) + => IdentifyAsync(Configuration.Default, filePath, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static async Task IdentifyAsync( + Configuration configuration, + string filePath, + CancellationToken cancellationToken = default) + { + (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, filePath, cancellationToken) + .ConfigureAwait(false); + return res.ImageInfo; + } + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + string filePath, + CancellationToken cancellationToken = default) + => IdentifyWithFormatAsync(Configuration.Default, filePath, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static async Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Configuration configuration, + string filePath, + CancellationToken cancellationToken = default) + { + Guard.NotNull(configuration, nameof(configuration)); + using Stream stream = configuration.FileSystem.OpenRead(filePath); + return await IdentifyWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + } /// /// Create a new instance of the class from the given file. @@ -97,9 +158,9 @@ public static Image Load(string path) /// /// Thrown if the stream is not readable nor seekable. /// - /// A representing the asynchronous operation. - public static Task LoadAsync(string path) - => LoadAsync(Configuration.Default, path); + /// The . + public static Image Load(string path) + => Load(Configuration.Default, path); /// /// Create a new instance of the class from the given file. @@ -131,18 +192,21 @@ public static Image Load(Configuration configuration, string path) /// /// The configuration for the decoder. /// The file path to the image. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The path is null. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task LoadAsync(Configuration configuration, string path) + public static async Task LoadAsync( + Configuration configuration, + string path, + CancellationToken cancellationToken = default) { - using (Stream stream = configuration.FileSystem.OpenRead(path)) - { - (Image img, _) = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); - return img; - } + using Stream stream = configuration.FileSystem.OpenRead(path); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + return img; } /// @@ -168,27 +232,140 @@ public static Image Load(Configuration configuration, string path, IImageDecoder } } + /// + /// 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 = default) + => LoadAsync(Configuration.Default, path, cancellationToken); + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The decoder. + /// 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, IImageDecoder decoder) + => LoadAsync(Configuration.Default, path, decoder, default); + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The decoder. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static Task> LoadAsync(string path, IImageDecoder decoder) + where TPixel : unmanaged, IPixel + => LoadAsync(Configuration.Default, path, decoder, default); + + /// + /// Create a new instance of the class from the given file. + /// + /// The Configuration. + /// The file path to the image. + /// The decoder. + /// 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( + Configuration configuration, + string path, + IImageDecoder decoder, + CancellationToken cancellationToken = default) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + + using Stream stream = configuration.FileSystem.OpenRead(path); + return LoadAsync(configuration, stream, decoder, cancellationToken); + } + /// /// Create a new instance of the class from the given file. /// /// The Configuration. /// The file path to the image. /// The decoder. + /// 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. + /// The pixel format. /// A representing the asynchronous operation. - public static Task LoadAsync(Configuration configuration, string path, IImageDecoder decoder) + public static Task> LoadAsync( + Configuration configuration, + string path, + IImageDecoder decoder, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(path, nameof(path)); - using (Stream stream = configuration.FileSystem.OpenRead(path)) - { - return LoadAsync(configuration, stream, decoder); - } + using Stream stream = configuration.FileSystem.OpenRead(path); + return LoadAsync(configuration, stream, decoder, cancellationToken); + } + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static Task> LoadAsync(string path) + where TPixel : unmanaged, IPixel + => LoadAsync(Configuration.Default, path, default(CancellationToken)); + + /// + /// Create a new instance of the class from the given file. + /// + /// The configuration for the decoder. + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static async Task> LoadAsync( + Configuration configuration, + string path, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + { + using Stream stream = configuration.FileSystem.OpenRead(path); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + return img; } /// diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index fae88f21e1..ee148cd254 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -62,7 +63,8 @@ public static Task DetectFormatAsync(Configuration configuration, => WithSeekableStreamAsync( configuration, stream, - s => InternalDetectFormatAsync(s, configuration)); + (s, _) => InternalDetectFormatAsync(s, configuration), + default); /// /// Reads the raw image information from the specified stream without fully decoding it. @@ -125,6 +127,7 @@ public static IImageInfo Identify(Configuration configuration, Stream stream) /// /// The configuration. /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -133,9 +136,12 @@ public static IImageInfo Identify(Configuration configuration, Stream stream) /// A representing the asynchronous operation or null if /// a suitable detector is not found. /// - public static async Task IdentifyAsync(Configuration configuration, Stream stream) + public static async Task IdentifyAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) { - (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, stream).ConfigureAwait(false); + (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, stream, cancellationToken).ConfigureAwait(false); return res.ImageInfo; } @@ -164,35 +170,43 @@ public static IImageInfo Identify(Configuration configuration, Stream stream, ou /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image contains invalid content. /// - /// A representing the asynchronous operation or null if - /// a suitable detector is not found. + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. /// - public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Stream stream) - => IdentifyWithFormatAsync(Configuration.Default, stream); + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Stream stream, + CancellationToken cancellationToken = default) + => IdentifyWithFormatAsync(Configuration.Default, stream, cancellationToken); /// /// Reads the raw image information from the specified stream without fully decoding it. /// /// The configuration. /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image contains invalid content. /// - /// The representing the asyncronous operation with the parameter type + /// The representing the asynchronous operation with the parameter type /// property set to null if suitable info detector is not found. /// - public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Configuration configuration, Stream stream) + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) => WithSeekableStreamAsync( configuration, stream, - s => InternalIdentityAsync(s, configuration ?? Configuration.Default)); + (s, ct) => InternalIdentityAsync(s, configuration ?? Configuration.Default, ct), + cancellationToken); /// /// Decode a new instance of the class from the given stream. @@ -302,6 +316,7 @@ public static Image Load(Configuration configuration, Stream stream, IImageDecod /// The configuration for the decoder. /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The decoder is null. @@ -309,13 +324,18 @@ public static Image Load(Configuration configuration, Stream stream, IImageDecod /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static Task LoadAsync(Configuration configuration, Stream stream, IImageDecoder decoder) + public static Task LoadAsync( + Configuration configuration, + Stream stream, + IImageDecoder decoder, + CancellationToken cancellationToken = default) { Guard.NotNull(decoder, nameof(decoder)); return WithSeekableStreamAsync( configuration, stream, - s => decoder.DecodeAsync(configuration, s)); + (s, ct) => decoder.DecodeAsync(configuration, s, ct), + cancellationToken); } /// @@ -336,15 +356,17 @@ public static Task LoadAsync(Configuration configuration, Stream stream, /// /// The configuration for the decoder. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task LoadAsync(Configuration configuration, Stream stream) + public static async Task LoadAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken = default) { - (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); return fmt.Image; } @@ -425,18 +447,20 @@ public static Image Load(Stream stream, IImageDecoder decoder) /// /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static Task> LoadAsync(Stream stream, IImageDecoder decoder) + public static Task> LoadAsync(Stream stream, IImageDecoder decoder, CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel => WithSeekableStreamAsync( Configuration.Default, stream, - s => decoder.DecodeAsync(Configuration.Default, s)); + (s, ct) => decoder.DecodeAsync(Configuration.Default, s, ct), + cancellationToken); /// /// Create a new instance of the class from the given stream. @@ -461,6 +485,7 @@ public static Image Load(Configuration configuration, Stream str /// The Configuration. /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -468,12 +493,17 @@ public static Image Load(Configuration configuration, Stream str /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static Task> LoadAsync(Configuration configuration, Stream stream, IImageDecoder decoder) + public static Task> LoadAsync( + Configuration configuration, + Stream stream, + IImageDecoder decoder, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel => WithSeekableStreamAsync( configuration, stream, - s => decoder.DecodeAsync(configuration, s)); + (s, ct) => decoder.DecodeAsync(configuration, s, ct), + cancellationToken); /// /// Create a new instance of the class from the given stream. @@ -532,18 +562,23 @@ public static Image Load(Configuration configuration, Stream str /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Configuration configuration, Stream stream) + public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) { (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( configuration, stream, - async s => await DecodeAsync(s, configuration).ConfigureAwait(false)) + async (s, ct) => await DecodeAsync(s, configuration, ct).ConfigureAwait(false), + cancellationToken) .ConfigureAwait(false); if (data.Image != null) @@ -567,6 +602,7 @@ public static Image Load(Configuration configuration, Stream str /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -574,14 +610,18 @@ public static Image Load(Configuration configuration, Stream str /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Configuration configuration, Stream stream) + public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel { (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( configuration, stream, - s => DecodeAsync(s, configuration)) + (s, ct) => DecodeAsync(s, configuration, ct), + cancellationToken) .ConfigureAwait(false); if (data.Image != null) @@ -605,6 +645,7 @@ await WithSeekableStreamAsync( /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -612,10 +653,14 @@ await WithSeekableStreamAsync( /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static async Task> LoadAsync(Configuration configuration, Stream stream) + public static async Task> LoadAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel { - (Image img, _) = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); return img; } @@ -700,11 +745,13 @@ private static T WithSeekableStream( /// The configuration. /// The input stream. /// The action to perform. + /// The cancellation token. /// The . private static async Task WithSeekableStreamAsync( Configuration configuration, Stream stream, - Func> action) + Func> action, + CancellationToken cancellationToken) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(stream, nameof(stream)); @@ -725,14 +772,14 @@ private static async Task WithSeekableStreamAsync( stream.Position = 0; } - return await action(stream).ConfigureAwait(false); + return await action(stream, cancellationToken).ConfigureAwait(false); } using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); - await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize).ConfigureAwait(false); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; - return await action(memoryStream).ConfigureAwait(false); + return await action(memoryStream, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 605f5e0da8..fbb3ec2069 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -103,15 +104,16 @@ public void Save(Stream stream, IImageEncoder encoder) /// /// The stream to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the stream or encoder is null. /// A representing the asynchronous operation. - public Task SaveAsync(Stream stream, IImageEncoder encoder) + public Task SaveAsync(Stream stream, IImageEncoder encoder, CancellationToken cancellationToken = default) { Guard.NotNull(stream, nameof(stream)); Guard.NotNull(encoder, nameof(encoder)); this.EnsureNotDisposed(); - return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)); + return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream), cancellationToken); } /// @@ -162,7 +164,8 @@ public abstract Image CloneAs(Configuration configuration) /// with the pixel type of the image. /// /// The visitor. - internal abstract Task AcceptAsync(IImageVisitorAsync visitor); + /// The token to monitor for cancellation requests. + internal abstract Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken); private class EncodeVisitor : IImageVisitor, IImageVisitorAsync { @@ -179,8 +182,8 @@ public EncodeVisitor(IImageEncoder encoder, Stream stream) public void Visit(Image image) where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream); - public Task VisitAsync(Image image) - where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream); + public Task VisitAsync(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream, cancellationToken); } } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index b227679a67..d40c5c271b 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -30,10 +31,11 @@ public static void Save(this Image source, string path) /// /// The source image. /// The file path to save the image to. + /// The token to monitor for cancellation requests. /// The path is null. /// A representing the asynchronous operation. - public static Task SaveAsync(this Image source, string path) - => source.SaveAsync(path, source.DetectEncoder(path)); + public static Task SaveAsync(this Image source, string path, CancellationToken cancellationToken = default) + => source.SaveAsync(path, source.DetectEncoder(path), cancellationToken); /// /// Writes the image to the given stream using the currently loaded image format. @@ -59,17 +61,20 @@ public static void Save(this Image source, string path, IImageEncoder encoder) /// The source image. /// The file path to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// The path is null. /// The encoder is null. /// A representing the asynchronous operation. - public static async Task SaveAsync(this Image source, string path, IImageEncoder encoder) + public static async Task SaveAsync( + this Image source, + string path, + IImageEncoder encoder, + CancellationToken cancellationToken = default) { Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) - { - await source.SaveAsync(fs, encoder).ConfigureAwait(false); - } + using Stream fs = source.GetConfiguration().FileSystem.Create(path); + await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 41ecee503f..c3d9618c8c 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -23,7 +23,7 @@ - + @@ -41,7 +41,7 @@ - + True True @@ -132,6 +132,11 @@ True PorterDuffFunctions.Generated.tt + + True + True + ImageExtensions.Save.tt + @@ -207,6 +212,10 @@ DefaultPixelBlenders.Generated.cs TextTemplatingFileGenerator + + TextTemplatingFileGenerator + ImageExtensions.Save.cs + diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 9d3abc1e82..255193c8ea 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -290,11 +291,11 @@ internal override void Accept(IImageVisitor visitor) } /// - internal override Task AcceptAsync(IImageVisitorAsync visitor) + internal override Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken) { this.EnsureNotDisposed(); - return visitor.VisitAsync(this); + return visitor.VisitAsync(this, cancellationToken); } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 912f606b2c..78218aec90 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 @@ public void JpegDecoder_IsNotBoundToSinglePixelType(TestImageProvider(TestImageProvider provider) + public void Decode_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) where TPixel : unmanaged, IPixel { provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); @@ -112,6 +114,60 @@ public void DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(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.Baseline.Jpeg420Small, 0)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 10)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 10)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)] + public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs) + { + // Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay. + string hugeFile = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + if (cancellationDelayMs == 0) + { + cts.Cancel(); + } + else + { + cts.CancelAfter(cancellationDelayMs); + } + + await Assert.ThrowsAsync(() => Image.LoadAsync(hugeFile, cts.Token)); + } + + [Theory(Skip = "Identify is too fast, doesn't work reliably.")] + [InlineData(TestImages.Jpeg.Baseline.Exif)] + [InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)] + public async Task Identify_IsCancellable(string fileName) + { + string file = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromTicks(1)); + await Assert.ThrowsAsync(() => Image.IdentifyAsync(file, 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/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 6c9a744633..981270a5fb 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 @@ public void Encode_PreservesIccProfile() 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.ThrowsAsync(() => image.SaveAsync(stream, encoder, cts.Token)); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs index 12e1ec22bd..0dd2abcc1f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs @@ -71,7 +71,7 @@ public void PostProcess(TestImageProvider provider) 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/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs new file mode 100644 index 0000000000..317a5129c4 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public partial class ImageTests + { + public class Decode_Cancellation : ImageLoadTestBase + { + private bool isTestStreamSeekable; + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore = new SemaphoreSlim(0); + private readonly SemaphoreSlim continueSemaphore = new SemaphoreSlim(0); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + + public Decode_Cancellation() + { + this.TopLevelConfiguration.StreamProcessingBufferSize = 128; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task LoadAsync_Specific_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task LoadAsync_Agnostic_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task LoadAsync_Agnostic_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Fact] + public async Task LoadAsync_Specific_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task IdentifyAsync_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task IdentifyAsync_CustomConfiguration_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task IdentifyWithFormatAsync_CustomConfiguration_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task IdentifyWithFormatAsync_CustomConfiguration_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Fact] + public async Task IdentifyWithFormatAsync_DefaultConfiguration_Stream() + { + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.DataStream, this.cts.Token)); + } + + private async Task DoCancel() + { + // wait until we reach the middle of the steam + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + + // set the cancellation + this.cts.Cancel(); + + // continue processing the stream + this.continueSemaphore.Release(); + } + + protected override Stream CreateStream() => this.TestFormat.CreateAsyncSamaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable); + } + } +} diff --git a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs index 6327b3eacd..5f3b16806f 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs @@ -24,8 +24,6 @@ public class DetectFormat : ImageLoadTestBase private ReadOnlySpan ActualImageSpan => this.ActualImageBytes.AsSpan(); - private byte[] ByteArray => this.DataStream.ToArray(); - private IImageFormat LocalImageFormat => this.localImageFormatMock.Object; private static readonly IImageFormat ExpectedGlobalFormat = diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs index 69b1d21a6d..72de3fcc44 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs @@ -19,9 +19,9 @@ public class Identify : ImageLoadTestBase { private static readonly string ActualImagePath = TestFile.GetInputFileFullPath(TestImages.Bmp.F); - private byte[] ActualImageBytes => TestFile.Create(TestImages.Bmp.F).Bytes; + private static readonly Size ExpectedImageSize = new Size(108, 202); - private byte[] ByteArray => this.DataStream.ToArray(); + private byte[] ActualImageBytes => TestFile.Create(TestImages.Bmp.F).Bytes; private IImageInfo LocalImageInfo => this.localImageInfoMock.Object; @@ -35,7 +35,7 @@ public void FromBytes_GlobalConfiguration() { IImageInfo info = Image.Identify(this.ActualImageBytes, out IImageFormat type); - Assert.NotNull(info); + Assert.Equal(ExpectedImageSize, info.Size()); Assert.Equal(ExpectedGlobalFormat, type); } @@ -133,13 +133,45 @@ public async Task FromStreamAsync_GlobalConfiguration() using (var stream = new MemoryStream(this.ActualImageBytes)) { var asyncStream = new AsyncStreamWrapper(stream, () => false); - (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(asyncStream); + (IImageInfo ImageInfo, IImageFormat Format) res = await Image.IdentifyWithFormatAsync(asyncStream); - Assert.NotNull(info.ImageInfo); - Assert.Equal(ExpectedGlobalFormat, info.Format); + Assert.Equal(ExpectedImageSize, res.ImageInfo.Size()); + Assert.Equal(ExpectedGlobalFormat, res.Format); } } + [Fact] + public async Task FromPathAsync_CustomConfiguration() + { + IImageInfo info = await Image.IdentifyAsync(this.LocalConfiguration, this.MockFilePath); + Assert.Equal(this.LocalImageInfo, info); + } + + [Fact] + public async Task IdentifyWithFormatAsync_FromPath_CustomConfiguration() + { + (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(this.LocalConfiguration, this.MockFilePath); + Assert.NotNull(info.ImageInfo); + Assert.Equal(this.LocalImageFormat, info.Format); + } + + [Fact] + public async Task IdentifyWithFormatAsync_FromPath_GlobalConfiguration() + { + (IImageInfo ImageInfo, IImageFormat Format) res = await Image.IdentifyWithFormatAsync(ActualImagePath); + + Assert.Equal(ExpectedImageSize, res.ImageInfo.Size()); + Assert.Equal(ExpectedGlobalFormat, res.Format); + } + + [Fact] + public async Task FromPathAsync_GlobalConfiguration() + { + IImageInfo info = await Image.IdentifyAsync(ActualImagePath); + + Assert.Equal(ExpectedImageSize, info.Size()); + } + [Fact] public async Task FromStreamAsync_CustomConfiguration() { diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index 70d572d60a..44d7daa740 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -3,7 +3,7 @@ using System; using System.IO; - +using System.Threading; using Moq; using SixLabors.ImageSharp.Formats; @@ -16,6 +16,8 @@ public partial class ImageTests { public abstract class ImageLoadTestBase : IDisposable { + private Lazy dataStreamLazy; + protected Image localStreamReturnImageRgba32; protected Image localStreamReturnImageAgnostic; @@ -46,10 +48,12 @@ public abstract class ImageLoadTestBase : IDisposable public byte[] Marker { get; } - public MemoryStream DataStream { get; } + public Stream DataStream => this.dataStreamLazy.Value; public byte[] DecodedData { get; private set; } + protected byte[] ByteArray => ((MemoryStream)this.DataStream).ToArray(); + protected ImageLoadTestBase() { this.localStreamReturnImageRgba32 = new Image(1, 1); @@ -60,10 +64,9 @@ protected ImageLoadTestBase() var detector = new Mock(); detector.Setup(x => x.Identify(It.IsAny(), It.IsAny())).Returns(this.localImageInfoMock.Object); - detector.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny())).ReturnsAsync(this.localImageInfoMock.Object); - this.localDecoder = detector.As(); - this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormatMock.Object); + detector.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(this.localImageInfoMock.Object); + this.localDecoder = detector.As(); this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny())) .Callback((c, s) => { @@ -86,6 +89,8 @@ protected ImageLoadTestBase() }) .Returns(this.localStreamReturnImageAgnostic); + this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormatMock.Object); + this.LocalConfiguration = new Configuration(); this.LocalConfiguration.ImageFormatsManager.AddImageFormatDetector(this.localMimeTypeDetector); this.LocalConfiguration.ImageFormatsManager.SetDecoder(this.localImageFormatMock.Object, this.localDecoder.Object); @@ -93,10 +98,12 @@ protected ImageLoadTestBase() this.TopLevelConfiguration = new Configuration(this.TestFormat); this.Marker = Guid.NewGuid().ToByteArray(); - this.DataStream = this.TestFormat.CreateStream(this.Marker); - this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(this.DataStream); - this.topLevelFileSystem.AddFile(this.MockFilePath, this.DataStream); + this.dataStreamLazy = new Lazy(this.CreateStream); + Stream StreamFactory() => this.DataStream; + + this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory); + this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory); this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object; this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem; } @@ -107,6 +114,8 @@ public void Dispose() this.localStreamReturnImageRgba32?.Dispose(); this.localStreamReturnImageAgnostic?.Dispose(); } + + protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs index 77e5679f64..a5034e43b0 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs @@ -25,84 +25,80 @@ private static void VerifyDecodedImage(Image img) [Fact] public void Path_Specific() { - using (var img = Image.Load(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path); + VerifyDecodedImage(img); } [Fact] public void Path_Agnostic() { - using (var img = Image.Load(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path); + VerifyDecodedImage(img); } [Fact] public async Task Path_Agnostic_Async() { - using (var img = await Image.LoadAsync(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); + } + + [Fact] + public async Task Path_Specific_Async() + { + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); } [Fact] public async Task Path_Agnostic_Configuration_Async() { - using (var img = await Image.LoadAsync(Configuration.Default, this.Path)) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); } [Fact] public void Path_Decoder_Specific() { - using (var img = Image.Load(this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public void Path_Decoder_Agnostic() { - using (var img = Image.Load(this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public async Task Path_Decoder_Agnostic_Async() { - using (var img = await Image.LoadAsync(Configuration.Default, this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); + } + + [Fact] + public async Task Path_Decoder_Specific_Async() + { + using var img = await Image.LoadAsync(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public void Path_OutFormat_Specific() { - using (var img = Image.Load(this.Path, out IImageFormat format)) - { - VerifyDecodedImage(img); - Assert.IsType(format); - } + using var img = Image.Load(this.Path, out IImageFormat format); + VerifyDecodedImage(img); + Assert.IsType(format); } [Fact] public void Path_OutFormat_Agnostic() { - using (var img = Image.Load(this.Path, out IImageFormat format)) - { - VerifyDecodedImage(img); - Assert.IsType(format); - } + using var img = Image.Load(this.Path, out IImageFormat format); + VerifyDecodedImage(img); + Assert.IsType(format); } [Fact] @@ -124,6 +120,20 @@ public void WhenPathIsNull_Throws() Image.Load((string)null); }); } + + [Fact] + public Task Async_WhenFileNotFound_Throws() + { + return Assert.ThrowsAsync( + () => Image.LoadAsync(Guid.NewGuid().ToString())); + } + + [Fact] + public Task Async_WhenPathIsNull_Throws() + { + return Assert.ThrowsAsync( + () => Image.LoadAsync((string)null)); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs index d275a11d8a..320f3696d6 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs @@ -14,8 +14,6 @@ public partial class ImageTests { public class Load_FromBytes_PassLocalConfiguration : ImageLoadTestBase { - private byte[] ByteArray => this.DataStream.ToArray(); - private ReadOnlySpan ByteSpan => this.ByteArray.AsSpan(); [Theory] diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 4a6c96ae89..40c3b65b53 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 @@ public async Task ThrowsWhenDisposed() [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 @@ public async Task SaveNeverCallsSyncMethods(string filename) } } } + + [Fact] + public async Task SaveAsync_WithNonSeekableStream_IsCancellable() + { + using var image = new Image(4000, 4000); + var encoder = new PngEncoder() { CompressionLevel = PngCompressionLevel.BestCompression }; + 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)); + } } } } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 98f8e95745..0761b0978d 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs index 960f136374..da43629567 100644 --- a/tests/ImageSharp.Tests/TestFileSystem.cs +++ b/tests/ImageSharp.Tests/TestFileSystem.cs @@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp.Tests /// public class TestFileSystem : ImageSharp.IO.IFileSystem { - private readonly Dictionary fileSystem = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> fileSystem = new Dictionary>(StringComparer.OrdinalIgnoreCase); - public void AddFile(string path, Stream data) + public void AddFile(string path, Func data) { lock (this.fileSystem) { @@ -29,7 +29,7 @@ public Stream Create(string path) { if (this.fileSystem.ContainsKey(path)) { - Stream stream = this.fileSystem[path]; + Stream stream = this.fileSystem[path](); stream.Position = 0; return stream; } @@ -45,7 +45,7 @@ public Stream OpenRead(string path) { if (this.fileSystem.ContainsKey(path)) { - Stream stream = this.fileSystem[path]; + Stream stream = this.fileSystem[path](); stream.Position = 0; return stream; } @@ -54,4 +54,4 @@ public Stream OpenRead(string path) return File.OpenRead(path); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index a883039f27..7273a65f79 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -6,9 +6,11 @@ using System.IO; using System.Linq; using System.Numerics; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests @@ -32,9 +34,9 @@ public TestFormat() public List DecodeCalls { get; } = new List(); - public IImageEncoder Encoder { get; } + public TestEncoder Encoder { get; } - public IImageDecoder Decoder { get; } + public TestDecoder Decoder { get; } private byte[] header = Guid.NewGuid().ToByteArray(); @@ -52,6 +54,14 @@ public MemoryStream CreateStream(byte[] marker = null) return ms; } + public Stream CreateAsyncSamaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512) + { + var buffer = new byte[size]; + this.header.CopyTo(buffer, 0); + var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore); + return seeakable ? (Stream)semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false); + } + public void VerifySpecificDecodeCall(byte[] marker, Configuration config) where TPixel : unmanaged, IPixel { @@ -187,7 +197,7 @@ public TestHeader(TestFormat testFormat) } } - public class TestDecoder : IImageDecoder + public class TestDecoder : IImageDecoder, IImageInfoDetector { private TestFormat testFormat; @@ -204,9 +214,17 @@ public TestDecoder(TestFormat testFormat) public Image Decode(Configuration config, Stream stream) where TPixel : unmanaged, IPixel + => this.DecodeImpl(config, stream, default).GetAwaiter().GetResult(); + + public Task> DecodeAsync(Configuration config, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.DecodeImpl(config, stream, cancellationToken); + + private async Task> DecodeImpl(Configuration config, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { var ms = new MemoryStream(); - stream.CopyTo(ms); + await stream.CopyToAsync(ms, config.StreamProcessingBufferSize, cancellationToken); var marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray(); this.testFormat.DecodeCalls.Add(new DecodeOperation { @@ -219,15 +237,18 @@ public Image Decode(Configuration config, Stream stream) return this.testFormat.Sample(); } - public Task> DecodeAsync(Configuration config, Stream stream) - where TPixel : unmanaged, IPixel - => Task.FromResult(this.Decode(config, stream)); - public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header); public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); + + public IImageInfo Identify(Configuration configuration, Stream stream) => + this.IdentifyAsync(configuration, stream, default).GetAwaiter().GetResult(); + + public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeImpl(configuration, stream, cancellationToken); } public class TestEncoder : ImageSharp.Formats.IImageEncoder @@ -249,7 +270,7 @@ public void Encode(Image image, Stream stream) // TODO record this happened so we can verify it. } - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO record this happened so we can verify it. diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 78567f9267..440baaa63b 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 @@ public override Image GetImage(IImageDecoder decoder) 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 da641a2969..700c40b726 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 @@ public virtual Image GetImage(IImageDecoder decoder) 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. /// diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index fae3ff5a5b..de8278a33e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using ImageMagick; using SixLabors.ImageSharp.Formats; @@ -46,7 +47,7 @@ private static void FromRgba64Bytes(Configuration configuration, Span> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel => Task.FromResult(this.Decode(configuration, stream)); @@ -82,6 +83,7 @@ public Image Decode(Configuration configuration, Stream stream) public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index ae6e2e3f16..1eb1328ef2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.Formats; @@ -14,7 +15,7 @@ public class SystemDrawingReferenceDecoder : IImageDecoder, IImageInfoDetector { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel => Task.FromResult(this.Decode(configuration, stream)); @@ -48,7 +49,7 @@ public Image Decode(Configuration configuration, Stream stream) } } - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) => Task.FromResult(this.Identify(configuration, stream)); public IImageInfo Identify(Configuration configuration, Stream stream) @@ -62,6 +63,7 @@ public IImageInfo Identify(Configuration configuration, Stream stream) public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs index 2aaf7bf80c..4c28079c1c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs @@ -3,6 +3,7 @@ using System.Drawing.Imaging; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -31,7 +32,7 @@ public void Encode(Image image, Stream stream) } } - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using (System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) diff --git a/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs b/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs new file mode 100644 index 0000000000..f03d2c4938 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + internal class SemaphoreReadMemoryStream : MemoryStream + { + private readonly SemaphoreSlim continueSemaphore; + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore; + private int pauseDone; + private readonly long waitPosition; + + public SemaphoreReadMemoryStream( + byte[] buffer, + long waitPosition, + SemaphoreSlim notifyWaitPositionReachedSemaphore, + SemaphoreSlim continueSemaphore) + : base(buffer) + { + this.continueSemaphore = continueSemaphore; + this.notifyWaitPositionReachedSemaphore = notifyWaitPositionReachedSemaphore; + this.waitPosition = waitPosition; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = base.Read(buffer, offset, count); + if (this.Position > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + this.continueSemaphore.Wait(); + } + + return read; + } + + private bool TryPause() => Interlocked.CompareExchange(ref this.pauseDone, 1, 0) == 0; + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = await base.ReadAsync(buffer, offset, count, cancellationToken); + if (this.Position > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + await this.continueSemaphore.WaitAsync(); + } + + return read; + } + + public override int ReadByte() + { + if (this.Position + 1 > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + this.continueSemaphore.Wait(); + } + + int result = base.ReadByte(); + return result; + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs new file mode 100644 index 0000000000..87f8cb8c17 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class SemaphoreReadMemoryStreamTests + { + private readonly SemaphoreSlim continueSemaphore = new SemaphoreSlim(0); + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore = new SemaphoreSlim(0); + private readonly byte[] buffer = new byte[128]; + + [Fact] + public void Read_BeforeWaitLimit_ShouldFinish() + { + using Stream stream = this.CreateTestStream(); + int read = stream.Read(this.buffer); + Assert.Equal(this.buffer.Length, read); + } + + [Fact] + public async Task ReadAsync_BeforeWaitLimit_ShouldFinish() + { + using Stream stream = this.CreateTestStream(); + int read = await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + Assert.Equal(this.buffer.Length, read); + } + + [Fact] + public async Task Read_AfterWaitLimit_ShouldPause() + { + using Stream stream = this.CreateTestStream(); + stream.Read(this.buffer); + Assert.Equal(0, this.notifyWaitPositionReachedSemaphore.CurrentCount); + + Task readTask = Task.Factory.StartNew( + () => + { + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + }, TaskCreationOptions.LongRunning); + + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + this.continueSemaphore.Release(); + await readTask; + } + + [Fact] + public async Task ReadAsync_AfterWaitLimit_ShouldPause() + { + using Stream stream = this.CreateTestStream(); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + + Task readTask = Task.Factory.StartNew( + async () => + { + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + }, TaskCreationOptions.LongRunning); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + this.continueSemaphore.Release(); + await readTask; + } + + private Stream CreateTestStream(int size = 1024, int waitAfterPosition = 256) + { + byte[] buffer = new byte[size]; + return new SemaphoreReadMemoryStream(buffer, waitAfterPosition, this.notifyWaitPositionReachedSemaphore, this.continueSemaphore); + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 978c0555cc..129d17f4df 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -371,7 +372,7 @@ public Image Decode(Configuration configuration, Stream stream) return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCountsAsync[this.callerName]++; @@ -391,7 +392,8 @@ internal void InitCaller(string name) public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } private class TestDecoderWithParameters : IImageDecoder @@ -425,7 +427,7 @@ public Image Decode(Configuration configuration, Stream stream) return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCountsAsync[this.callerName]++; @@ -445,7 +447,8 @@ internal void InitCaller(string name) public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } }