diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index d0e4338b3f..5791c6e92b 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Linq; -using System.Runtime.InteropServices; - +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -15,15 +17,66 @@ namespace SixLabors.ImageSharp.Advanced /// public static class AdvancedImageExtensions { + /// + /// For a given file path find the best encoder to use via its extension. + /// + /// The source image. + /// The target file path to save the image to. + /// The matching encoder. + public static IImageEncoder DetectEncoder(this Image source, string filePath) + { + Guard.NotNull(filePath, nameof(filePath)); + + string ext = Path.GetExtension(filePath); + IImageFormat format = source.GetConfiguration().ImageFormatsManager.FindFormatByFileExtension(ext); + if (format is null) + { + var sb = new StringBuilder(); + sb.AppendLine($"No encoder was found for extension '{ext}'. Registered encoders include:"); + foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats) + { + sb.AppendFormat(" - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); + } + + throw new NotSupportedException(sb.ToString()); + } + + IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); + + if (encoder is null) + { + var sb = new StringBuilder(); + sb.AppendLine($"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); + foreach (KeyValuePair enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + { + sb.AppendFormat(" - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); + } + + throw new NotSupportedException(sb.ToString()); + } + + return encoder; + } + /// /// Accepts a to implement a double-dispatch pattern in order to /// apply pixel-specific operations on non-generic instances /// - /// The source. - /// The visitor. + /// The source image. + /// The image visitor. public static void AcceptVisitor(this Image source, IImageVisitor visitor) => source.Accept(visitor); + /// + /// Accepts a to implement a double-dispatch pattern in order to + /// apply pixel-specific operations on non-generic instances + /// + /// The source image. + /// The image visitor. + /// A representing the asynchronous operation. + public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor) + => source.AcceptAsync(visitor); + /// /// Gets the configuration for the image. /// diff --git a/src/ImageSharp/Advanced/IImageVisitor.cs b/src/ImageSharp/Advanced/IImageVisitor.cs index 2c54c8f1f8..fbfdafeb10 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.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Advanced @@ -19,4 +20,20 @@ public interface IImageVisitor void Visit(Image image) where TPixel : unmanaged, IPixel; } + + /// + /// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations + /// on non-generic instances. + /// + public interface IImageVisitorAsync + { + /// + /// Provides a pixel-specific implementation for a given operation. + /// + /// The image. + /// The pixel type. + /// A representing the asynchronous operation. + Task VisitAsync(Image image) + where TPixel : unmanaged, IPixel; + } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 7865c06f42..16da086c9e 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.Tasks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -27,6 +28,26 @@ public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions, IImageInfoDe /// public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black; + /// + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new BmpDecoderCore(configuration, this); + + try + { + return await decoder.DecodeAsync(stream).ConfigureAwait(false); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + throw new InvalidImageContentException($"Can not 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); + } + } + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel @@ -50,6 +71,9 @@ 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).ConfigureAwait(false); + /// public IImageInfo Identify(Configuration configuration, Stream stream) { @@ -57,5 +81,13 @@ public IImageInfo Identify(Configuration configuration, Stream stream) return new BmpDecoderCore(configuration, this).Identify(stream); } + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new BmpDecoderCore(configuration, this).IdentifyAsync(stream); + } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index f97e810017..e37144bd58 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -7,7 +7,9 @@ using System.IO; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -130,8 +132,40 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options) /// is null. /// /// The decoded image. - public Image Decode(Stream stream) + public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel + { + // if we can seek then we arn't in a context that errors on async operations + if (stream.CanSeek) + { + return this.Decode(stream); + } + else + { + // cheat for now do async copy of the stream into memory stream and use the sync version + // we should use an array pool backed memorystream implementation + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } + } + } + + /// + /// Decodes the image from the specified this._stream and sets + /// the data to image. + /// + /// The pixel format. + /// The stream, where the image should be + /// decoded from. Cannot be null (Nothing in Visual Basic). + /// + /// is null. + /// + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel { try { @@ -218,6 +252,20 @@ public IImageInfo Identify(Stream stream) return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata); } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public async Task IdentifyAsync(Stream stream) + { + using (var ms = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } + } + /// /// Returns the y- value based on the given height. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index fa832b3a3a..08c9bde00d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// 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.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -39,5 +40,13 @@ public void Encode(Image image, Stream stream) var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); encoder.Encode(image, stream); } + + /// + public Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream); + } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index f2c3791f39..b3f64eea6a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -5,7 +5,7 @@ using System.Buffers; using System.IO; using System.Runtime.InteropServices; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; @@ -97,8 +97,32 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + 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) + 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 7e80859652..5f4fdd0fa6 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -24,6 +25,27 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe /// public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; + /// + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + var decoder = new GifDecoderCore(configuration, this); + + try + { + return await decoder.DecodeAsync(stream).ConfigureAwait(false); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + GifThrowHelper.ThrowInvalidImageContentException($"Can not 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; + } + } + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel @@ -54,7 +76,19 @@ public IImageInfo Identify(Configuration configuration, Stream stream) return decoder.Identify(stream); } + /// + public Task IdentifyAsync(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new GifDecoderCore(configuration, this); + return decoder.IdentifyAsync(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).ConfigureAwait(false); } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 649133085d..8f8426780d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -6,7 +6,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; - +using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -103,8 +104,32 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) /// The pixel format. /// The stream containing image data. /// The decoded image - public Image Decode(Stream stream) + public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel + { + if (stream.CanSeek) + { + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } + } + } + + /// + /// Decodes the stream to the image. + /// + /// The pixel format. + /// The stream containing image data. + /// The decoded image + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel { Image image = null; ImageFrame previousFrame = null; @@ -163,6 +188,27 @@ public Image Decode(Stream stream) return image; } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public async Task IdentifyAsync(Stream stream) + { + if (stream.CanSeek) + { + return this.Identify(stream); + } + else + { + using (var ms = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 5ded825e4d..539ab0fb38 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.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -38,5 +39,13 @@ public void Encode(Image image, Stream stream) var encoder = new GifEncoderCore(image.GetConfiguration(), this); encoder.Encode(image, stream); } + + /// + public Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encoder = new GifEncoderCore(image.GetConfiguration(), this); + return encoder.EncodeAsync(image, stream); + } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index fe77ba2896..556ace203a 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.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -74,8 +75,25 @@ public GifEncoderCore(Configuration configuration, IGifEncoderOptions options) /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public async Task EncodeAsync(Image image, Stream stream) 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/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index 77d13842a8..97886e5269 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.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -30,5 +31,25 @@ Image Decode(Configuration configuration, Stream stream) /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) Image Decode(Configuration configuration, Stream stream); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The configuration for the image. + /// The containing image data. + /// The . + // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) + Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an . + /// + /// The configuration for the image. + /// The containing image data. + /// The . + // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) + Task DecodeAsync(Configuration configuration, Stream stream); } } diff --git a/src/ImageSharp/Formats/IImageEncoder.cs b/src/ImageSharp/Formats/IImageEncoder.cs index 9e20944695..646e0ecc09 100644 --- a/src/ImageSharp/Formats/IImageEncoder.cs +++ b/src/ImageSharp/Formats/IImageEncoder.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -19,5 +20,15 @@ public interface IImageEncoder /// The to encode the image data to. void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel; + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// A representing the asynchronous operation. + Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 77abfe78f6..862c64999b 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading.Tasks; namespace SixLabors.ImageSharp.Formats { @@ -17,5 +18,13 @@ public interface IImageInfoDetector /// The containing image data. /// The object IImageInfo Identify(Configuration configuration, Stream stream); + + /// + /// Reads the raw image information from the specified stream. + /// + /// The configuration for the image. + /// The containing image data. + /// The object + Task IdentifyAsync(Configuration configuration, Stream stream); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index a13d7562ed..2d2d7fb56e 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.Tasks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -17,6 +18,28 @@ public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfo /// public bool IgnoreMetadata { get; set; } + /// + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + using var decoder = new JpegDecoderCore(configuration, this); + try + { + return await decoder.DecodeAsync(stream).ConfigureAwait(false); + } + catch (InvalidMemoryOperationException ex) + { + (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); + + JpegThrowHelper.ThrowInvalidImageContentException($"Can not 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; + } + } + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel @@ -43,6 +66,10 @@ 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).ConfigureAwait(false); + /// public IImageInfo Identify(Configuration configuration, Stream stream) { @@ -53,5 +80,16 @@ public IImageInfo Identify(Configuration configuration, Stream stream) return decoder.Identify(stream); } } + + /// + public async Task IdentifyAsync(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + using (var decoder = new JpegDecoderCore(configuration, this)) + { + return await decoder.IdentifyAsync(stream).ConfigureAwait(false); + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 15f9e36111..af1a705d44 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -6,6 +6,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -218,8 +219,32 @@ public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStr /// The pixel format. /// The stream, where the image should be. /// The decoded image. - public Image Decode(Stream stream) + public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel + { + if (stream.CanSeek) + { + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } + } + } + + /// + /// Decodes the image from the specified and sets the data to image. + /// + /// The pixel format. + /// The stream, where the image should be. + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel { this.ParseStream(stream); this.InitExifProfile(); @@ -229,6 +254,27 @@ public Image Decode(Stream stream) return this.PostProcessIntoImage(); } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public async Task IdentifyAsync(Stream stream) + { + if (stream.CanSeek) + { + return this.Identify(stream); + } + else + { + using (var ms = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 2d6b0d49dd..08d793401e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -35,5 +36,33 @@ public void Encode(Image image, Stream stream) var encoder = new JpegEncoderCore(this); encoder.Encode(image, stream); } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// A representing the asynchronous operation. + public async Task EncodeAsync(Image image, Stream stream) + 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); + } + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 3acc8ecea2..f755028dbd 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.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -194,7 +195,7 @@ public JpegEncoderCore(IJpegEncoderOptions options) /// The image to write from. /// The stream to write to. public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 9c728fc888..a6a040789a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -2,31 +2,15 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png { /// - /// Encoder for generating an image out of a png encoded stream. + /// Decoder for generating an image out of a png encoded stream. /// - /// - /// At the moment the following features are supported: - /// - /// Filters: all filters are supported. - /// - /// - /// Pixel formats: - /// - /// RGBA (True color) with alpha (8 bit). - /// RGB (True color) without alpha (8 bit). - /// grayscale with alpha (8 bit). - /// grayscale without alpha (8 bit). - /// Palette Index with alpha (8 bit). - /// Palette Index without alpha (8 bit). - /// - /// - /// public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDetector { /// @@ -34,6 +18,33 @@ public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDe /// public bool IgnoreMetadata { get; set; } + /// + /// Decodes the image from the specified stream to the . + /// + /// The pixel format. + /// The configuration for the image. + /// The containing image data. + /// The decoded image. + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + var decoder = new PngDecoderCore(configuration, this); + + try + { + return await decoder.DecodeAsync(stream).ConfigureAwait(false); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + PngThrowHelper.ThrowInvalidImageContentException($"Can not 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; + } + } + /// /// Decodes the image from the specified stream to the . /// @@ -68,7 +79,17 @@ public IImageInfo Identify(Configuration configuration, Stream stream) return decoder.Identify(stream); } + /// + public Task IdentifyAsync(Configuration configuration, Stream stream) + { + var decoder = new PngDecoderCore(configuration, this); + return decoder.IdentifyAsync(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).ConfigureAwait(false); } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5919b7537b..bb8589de49 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -9,10 +9,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -144,8 +145,38 @@ public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) /// Thrown if the image is larger than the maximum allowable size. /// /// The decoded image. - public Image Decode(Stream stream) + public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel + { + if (stream.CanSeek) + { + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } + } + } + + /// + /// Decodes the stream to the image. + /// + /// The pixel format. + /// The stream containing image data. + /// + /// Thrown if the stream does not contain and end chunk. + /// + /// + /// Thrown if the image is larger than the maximum allowable size. + /// + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); @@ -235,6 +266,27 @@ public Image Decode(Stream stream) } } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public async Task IdentifyAsync(Stream stream) + { + if (stream.CanSeek) + { + return this.Identify(stream); + } + else + { + using (var ms = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index cd6287fb26..61ea7c4684 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.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -63,5 +64,21 @@ public void Encode(Image image, Stream stream) encoder.Encode(image, stream); } } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// A representing the asynchronous operation. + public async Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) + { + await encoder.EncodeAsync(image, stream).ConfigureAwait(false); + } + } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 0ce516d2d1..a39fdd91f7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,7 +7,8 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; @@ -126,8 +127,32 @@ public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configurati /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + 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) + where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 746e88f4d3..06b9ab6050 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.Tasks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -12,6 +13,29 @@ namespace SixLabors.ImageSharp.Formats.Tga /// public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector { + /// + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new TgaDecoderCore(configuration, this); + + try + { + return await decoder.DecodeAsync(stream).ConfigureAwait(false); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + TgaThrowHelper.ThrowInvalidImageContentException($"Can not 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; + } + } + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel @@ -38,6 +62,9 @@ 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).ConfigureAwait(false); + /// public IImageInfo Identify(Configuration configuration, Stream stream) { @@ -45,5 +72,13 @@ public IImageInfo Identify(Configuration configuration, Stream stream) return new TgaDecoderCore(configuration, this).Identify(stream); } + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new TgaDecoderCore(configuration, this).IdentifyAsync(stream); + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e6648039e9..ae8946173b 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -5,7 +5,8 @@ using System.Buffers; using System.IO; using System.Runtime.CompilerServices; - +using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -88,8 +89,35 @@ public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) /// is null. /// /// The decoded image. - public Image Decode(Stream stream) + public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel + { + if (stream.CanSeek) + { + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } + } + } + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// + /// is null. + /// + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel { try { @@ -650,6 +678,27 @@ private void ReadRle(int width, int height, Buffer2D pixels, int } } + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public async Task IdentifyAsync(Stream stream) + { + if (stream.CanSeek) + { + return this.Identify(stream); + } + else + { + using (var ms = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 2d944db6c1..c8f8fb1a51 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -30,5 +30,13 @@ public void Encode(Image image, Stream stream) var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); encoder.Encode(image, stream); } + + /// + public Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream); + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 317bec0809..d9e7c0e6bc 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -5,7 +5,7 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -55,6 +55,30 @@ public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocato this.compression = options.Compression; } + /// + /// Encodes the image to the specified stream from the . + /// + /// 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 . /// diff --git a/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs b/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs new file mode 100644 index 0000000000..878fcc53a7 --- /dev/null +++ b/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.IO; + +namespace SixLabors.ImageSharp.IO +{ + /// + /// A memory stream constructed from a pooled buffer of known length. + /// + internal sealed class FixedCapacityPooledMemoryStream : MemoryStream + { + private readonly byte[] buffer; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The length of the stream buffer to rent. + public FixedCapacityPooledMemoryStream(long length) + : this(RentBuffer(length)) => this.Length = length; + + private FixedCapacityPooledMemoryStream(byte[] buffer) + : base(buffer) => this.buffer = buffer; + + /// + public override long Length { get; } + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + this.isDisposed = true; + + if (disposing) + { + ArrayPool.Shared.Return(this.buffer); + } + + base.Dispose(disposing); + } + } + + // In the extrememly unlikely event someone ever gives us a stream + // with length longer than int.MaxValue then we'll use something else. + private static byte[] RentBuffer(long length) => ArrayPool.Shared.Rent((int)length); + } +} diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index fca36b33c7..6b6dc6e21f 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.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -70,6 +71,20 @@ private static IImageFormat InternalDetectFormat(Stream stream, Configuration co } } + /// + /// By reading the header on the provided stream this calculates the images format. + /// + /// The image stream to read the header from. + /// The configuration. + /// The mime type or null if none found. + private static Task InternalDetectFormatAsync(Stream stream, Configuration config) + { + // We are going to cheat here because we know that by this point we have been wrapped in a + // seekable stream then we are free to use sync APIs this is potentially brittle and may + // need a better fix in the future. + return Task.FromResult(InternalDetectFormat(stream, config)); + } + /// /// By reading the header on the provided stream this calculates the images format. /// @@ -86,7 +101,6 @@ private static IImageDecoder DiscoverDecoder(Stream stream, Configuration config : null; } -#pragma warning disable SA1008 // Opening parenthesis must be spaced correctly /// /// Decodes the image stream to the current image. /// @@ -96,8 +110,7 @@ private static IImageDecoder DiscoverDecoder(Stream stream, Configuration config /// /// A new . /// - private static (Image img, IImageFormat format) Decode(Stream stream, Configuration config) -#pragma warning restore SA1008 // Opening parenthesis must be spaced correctly + private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config) where TPixel : unmanaged, IPixel { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); @@ -110,7 +123,27 @@ private static (Image img, IImageFormat format) Decode(Stream st return (img, format); } - private static (Image img, IImageFormat format) Decode(Stream stream, Configuration config) + /// + /// Decodes the image stream to the current image. + /// + /// The stream. + /// the configuration. + /// The pixel format. + /// A representing the asynchronous operation. + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + where TPixel : unmanaged, IPixel + { + IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); + if (decoder is null) + { + return (null, null); + } + + Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + return (img, format); + } + + private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config) { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); if (decoder is null) @@ -122,22 +155,60 @@ private static (Image img, IImageFormat format) Decode(Stream stream, Configurat return (img, format); } + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + { + IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); + if (decoder is null) + { + return (null, null); + } + + Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + return (img, format); + } + /// /// Reads the raw image information from the specified stream. /// /// The stream. /// the configuration. /// - /// The or null if suitable info detector not found. + /// The or null if a suitable info detector is not found. /// - private static (IImageInfo info, IImageFormat format) InternalIdentity(Stream stream, Configuration config) + private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(Stream stream, Configuration config) + { + if (!(DiscoverDecoder(stream, config, out IImageFormat format) is IImageInfoDetector detector)) + { + return (null, null); + } + + IImageInfo info = detector?.Identify(config, stream); + return (info, format); + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The stream. + /// the configuration. + /// + /// 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) { if (!(DiscoverDecoder(stream, config, out IImageFormat format) is IImageInfoDetector detector)) { return (null, null); } - return (detector?.Identify(config, stream), format); + if (detector is null) + { + return (null, format); + } + + IImageInfo info = await detector.IdentifyAsync(config, stream).ConfigureAwait(false); + return (info, format); } } } diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 6e22ee8a3d..499f5ac195 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -37,6 +39,31 @@ public static IImageFormat DetectFormat(Stream stream) public static IImageFormat DetectFormat(Configuration configuration, Stream stream) => WithSeekableStream(configuration, stream, s => InternalDetectFormat(s, configuration)); + /// + /// By reading the header on the provided stream this calculates the images format type. + /// + /// The image stream to read the header from. + /// The stream is null. + /// The stream is not readable. + /// A representing the asynchronous operation or null if none is found. + public static Task DetectFormatAsync(Stream stream) + => DetectFormatAsync(Configuration.Default, stream); + + /// + /// By reading the header on the provided stream this calculates the images format type. + /// + /// The configuration. + /// The image stream to read the header from. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// A representing the asynchronous operation. + public static Task DetectFormatAsync(Configuration configuration, Stream stream) + => WithSeekableStreamAsync( + configuration, + stream, + s => InternalDetectFormatAsync(s, configuration)); + /// /// Reads the raw image information from the specified stream without fully decoding it. /// @@ -45,11 +72,25 @@ public static IImageFormat DetectFormat(Configuration configuration, Stream stre /// The stream is not readable. /// Image contains invalid content. /// - /// The or null if suitable info detector not found. + /// The or null if a suitable info detector is not found. /// public static IImageInfo Identify(Stream stream) => Identify(stream, out IImageFormat _); + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image stream to read the header from. + /// 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. + /// + public static Task IdentifyAsync(Stream stream) + => IdentifyAsync(Configuration.Default, stream); + /// /// Reads the raw image information from the specified stream without fully decoding it. /// @@ -59,11 +100,45 @@ public static IImageInfo Identify(Stream stream) /// The stream is not readable. /// Image contains invalid content. /// - /// The or null if suitable info detector not found. + /// The or null if a suitable info detector is not found. /// public static IImageInfo Identify(Stream stream, out IImageFormat format) => Identify(Configuration.Default, stream, out format); + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image stream to read the information from. + /// The configuration is null. + /// The stream is null. + /// The stream is not readable. + /// Image contains invalid content. + /// + /// The or null if a suitable info detector is not found. + /// + public static IImageInfo Identify(Configuration configuration, Stream stream) + => Identify(configuration, stream, out _); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image stream to read the information from. + /// 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. + /// + public static async Task IdentifyAsync(Configuration configuration, Stream stream) + { + (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, stream).ConfigureAwait(false); + return res.ImageInfo; + } + /// /// Reads the raw image information from the specified stream without fully decoding it. /// @@ -75,16 +150,50 @@ public static IImageInfo Identify(Stream stream, out IImageFormat format) /// The stream is not readable. /// Image contains invalid content. /// - /// The or null if suitable info detector is not found. + /// The or null if a suitable info detector is not found. /// public static IImageInfo Identify(Configuration configuration, Stream stream, out IImageFormat format) { - (IImageInfo info, IImageFormat format) data = WithSeekableStream(configuration, stream, s => InternalIdentity(s, configuration ?? Configuration.Default)); + (IImageInfo ImageInfo, IImageFormat Format) data = WithSeekableStream(configuration, stream, s => InternalIdentity(s, configuration ?? Configuration.Default)); - format = data.format; - return data.info; + format = data.Format; + return data.ImageInfo; } + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image stream to read the information from. + /// 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. + /// + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Stream stream) + => IdentifyWithFormatAsync(Configuration.Default, stream); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image stream to read the information from. + /// 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 + /// property set to null if suitable info detector is not found. + /// + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Configuration configuration, Stream stream) + => WithSeekableStreamAsync( + configuration, + stream, + s => InternalIdentityAsync(s, configuration ?? Configuration.Default)); + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -99,6 +208,19 @@ public static IImageInfo Identify(Configuration configuration, Stream stream, ou public static Image Load(Stream stream, out IImageFormat format) => Load(Configuration.Default, stream, out format); + /// + /// Decode a new instance of the class from the given stream. + /// The pixel format is selected by the decoder. + /// + /// The stream containing image information. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Stream stream) + => LoadWithFormatAsync(Configuration.Default, stream); + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -111,6 +233,18 @@ public static Image Load(Stream stream, out IImageFormat format) /// The . public static Image Load(Stream stream) => Load(Configuration.Default, stream); + /// + /// Decode a new instance of the class from the given stream. + /// The pixel format is selected by the decoder. + /// + /// The stream containing image information. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(Stream stream) => LoadAsync(Configuration.Default, stream); + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -126,6 +260,21 @@ public static Image Load(Stream stream, out IImageFormat format) public static Image Load(Stream stream, IImageDecoder decoder) => Load(Configuration.Default, stream, decoder); + /// + /// Decode a new instance of the class from the given stream. + /// The pixel format is selected by the decoder. + /// + /// The stream containing image information. + /// The decoder. + /// The stream is null. + /// The decoder is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(Stream stream, IImageDecoder decoder) + => LoadAsync(Configuration.Default, stream, decoder); + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -139,13 +288,36 @@ public static Image Load(Stream stream, IImageDecoder decoder) /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. - /// A new .> + /// A new . public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) { Guard.NotNull(decoder, nameof(decoder)); return WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); } + /// + /// Decode a new instance of the class from the given stream. + /// The pixel format is selected by the decoder. + /// + /// The configuration for the decoder. + /// The stream containing image information. + /// The decoder. + /// The configuration is null. + /// The stream is null. + /// The decoder is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(Configuration configuration, Stream stream, IImageDecoder decoder) + { + Guard.NotNull(decoder, nameof(decoder)); + return WithSeekableStreamAsync( + configuration, + stream, + s => decoder.DecodeAsync(configuration, s)); + } + /// /// Decode a new instance of the class from the given stream. /// @@ -156,9 +328,26 @@ public static Image Load(Configuration configuration, Stream stream, IImageDecod /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. - /// A new .> + /// A new . public static Image Load(Configuration configuration, Stream stream) => Load(configuration, stream, out _); + /// + /// Decode a new instance of the class from the given stream. + /// + /// The configuration for the decoder. + /// The stream containing image information. + /// 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) + { + (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + return fmt.Image; + } + /// /// Create a new instance of the class from the given stream. /// @@ -168,11 +357,25 @@ public static Image Load(Configuration configuration, Stream stream, IImageDecod /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new .> + /// A new . public static Image Load(Stream stream) where TPixel : unmanaged, IPixel => Load(Configuration.Default, stream); + /// + /// Create a new instance of the class from the given stream. + /// + /// The stream containing image information. + /// 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) + where TPixel : unmanaged, IPixel + => LoadAsync(Configuration.Default, stream); + /// /// Create a new instance of the class from the given stream. /// @@ -183,11 +386,25 @@ public static Image Load(Stream stream) /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new .> + /// A new . public static Image Load(Stream stream, out IImageFormat format) where TPixel : unmanaged, IPixel => Load(Configuration.Default, stream, out format); + /// + /// Create a new instance of the class from the given stream. + /// + /// The stream containing image information. + /// 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 async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Stream stream) + where TPixel : unmanaged, IPixel + => await LoadWithFormatAsync(Configuration.Default, stream).ConfigureAwait(false); + /// /// Create a new instance of the class from the given stream. /// @@ -198,11 +415,29 @@ public static Image Load(Stream stream, out IImageFormat format) /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new .> + /// A new . public static Image Load(Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel => WithSeekableStream(Configuration.Default, stream, s => decoder.Decode(Configuration.Default, s)); + /// + /// Create a new instance of the class from the given stream. + /// + /// The stream containing image information. + /// The decoder. + /// 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) + where TPixel : unmanaged, IPixel + => WithSeekableStreamAsync( + Configuration.Default, + stream, + s => decoder.DecodeAsync(Configuration.Default, s)); + /// /// Create a new instance of the class from the given stream. /// @@ -215,11 +450,31 @@ public static Image Load(Stream stream, IImageDecoder decoder) /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new .> + /// A new . public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel => WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + /// + /// Create a new instance of the class from the given stream. + /// + /// The Configuration. + /// The stream containing image information. + /// The decoder. + /// The configuration is null. + /// 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(Configuration configuration, Stream stream, IImageDecoder decoder) + where TPixel : unmanaged, IPixel + => WithSeekableStreamAsync( + configuration, + stream, + s => decoder.DecodeAsync(configuration, s)); + /// /// Create a new instance of the class from the given stream. /// @@ -231,7 +486,7 @@ public static Image Load(Configuration configuration, Stream str /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new .> + /// A new . public static Image Load(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel => Load(configuration, stream, out IImageFormat _); @@ -248,17 +503,17 @@ public static Image Load(Configuration configuration, Stream str /// Image format not recognised. /// Image contains invalid content. /// The pixel format. - /// A new . + /// A representing the asynchronous operation. public static Image Load(Configuration configuration, Stream stream, out IImageFormat format) where TPixel : unmanaged, IPixel { - (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); + (Image Image, IImageFormat Format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); - format = data.format; + format = data.Format; - if (data.img != null) + if (data.Image != null) { - return data.img; + return data.Image; } var sb = new StringBuilder(); @@ -272,6 +527,98 @@ public static Image Load(Configuration configuration, Stream str throw new UnknownImageFormatException(sb.ToString()); } + /// + /// Create a new instance of the class from the given stream. + /// + /// The configuration options. + /// The stream containing image information. + /// 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) + { + (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( + configuration, + stream, + async s => await DecodeAsync(s, configuration).ConfigureAwait(false)) + .ConfigureAwait(false); + + if (data.Image != null) + { + return data; + } + + var sb = new StringBuilder(); + sb.AppendLine("Image cannot be loaded. Available decoders:"); + + foreach (KeyValuePair val in configuration.ImageFormatsManager.ImageDecoders) + { + sb.AppendFormat(" - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); + } + + throw new UnknownImageFormatException(sb.ToString()); + } + + /// + /// Create a new instance of the class from the given stream. + /// + /// The configuration options. + /// The stream containing image information. + /// The configuration is null. + /// 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 async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + (Image Image, IImageFormat Format) data = + await WithSeekableStreamAsync( + configuration, + stream, + s => DecodeAsync(s, configuration)) + .ConfigureAwait(false); + + if (data.Image != null) + { + return data; + } + + var sb = new StringBuilder(); + sb.AppendLine("Image cannot be loaded. Available decoders:"); + + foreach (KeyValuePair val in configuration.ImageFormatsManager.ImageDecoders) + { + sb.AppendFormat(" - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); + } + + throw new UnknownImageFormatException(sb.ToString()); + } + + /// + /// Create a new instance of the class from the given stream. + /// + /// The configuration options. + /// The stream containing image information. + /// The configuration is null. + /// 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 async Task> LoadAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + (Image img, _) = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + return img; + } + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -328,7 +675,7 @@ private static T WithSeekableStream(Configuration configuration, Stream strea } // We want to be able to load images from things like HttpContext.Request.Body - using (var memoryStream = new MemoryStream()) + using (var memoryStream = new FixedCapacityPooledMemoryStream(stream.Length)) { stream.CopyTo(memoryStream); memoryStream.Position = 0; @@ -336,5 +683,41 @@ private static T WithSeekableStream(Configuration configuration, Stream strea return action(memoryStream); } } + + private static async Task WithSeekableStreamAsync( + Configuration configuration, + Stream stream, + Func> action) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + // To make sure we don't trigger anything with aspnetcore then we just need to make sure we are + // seekable and we make the copy using CopyToAsync if the stream is seekable then we arn't using + // one of the aspnetcore wrapped streams that error on sync api calls and we can use it without + // having to further wrap + if (stream.CanSeek) + { + if (configuration.ReadOrigin == ReadOrigin.Begin) + { + stream.Position = 0; + } + + return await action(stream).ConfigureAwait(false); + } + + using (var memoryStream = new FixedCapacityPooledMemoryStream(stream.Length)) + { + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + + return await action(memoryStream).ConfigureAwait(false); + } + } } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index d84e0c4a57..7800a5c6f2 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -3,7 +3,7 @@ using System; using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata; @@ -98,6 +98,22 @@ public void Save(Stream stream, IImageEncoder encoder) this.AcceptVisitor(new EncodeVisitor(encoder, stream)); } + /// + /// Saves the image to the given stream using the given image encoder. + /// + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream or encoder is null. + /// A representing the asynchronous operation. + public async Task SaveAsync(Stream stream, IImageEncoder encoder) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(encoder, nameof(encoder)); + this.EnsureNotDisposed(); + + await this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)).ConfigureAwait(false); + } + /// /// Returns a copy of the image in the given pixel format. /// @@ -140,7 +156,15 @@ public abstract Image CloneAs(Configuration configuration) /// The visitor. internal abstract void Accept(IImageVisitor visitor); - private class EncodeVisitor : IImageVisitor + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + internal abstract Task AcceptAsync(IImageVisitorAsync visitor); + + private class EncodeVisitor : IImageVisitor, IImageVisitorAsync { private readonly IImageEncoder encoder; @@ -153,10 +177,11 @@ public EncodeVisitor(IImageEncoder encoder, Stream stream) } public void Visit(Image image) + where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream); + + public async Task VisitAsync(Image image) where TPixel : unmanaged, IPixel - { - this.encoder.Encode(image, this.stream); - } + => await this.encoder.EncodeAsync(image, this.stream).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index be06c836f1..b227679a67 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.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -22,38 +23,34 @@ public static partial class ImageExtensions /// The file path to save the image to. /// The path is null. public static void Save(this Image source, string path) - { - Guard.NotNull(path, nameof(path)); - - string ext = Path.GetExtension(path); - IImageFormat format = source.GetConfiguration().ImageFormatsManager.FindFormatByFileExtension(ext); - if (format is null) - { - var sb = new StringBuilder(); - sb.AppendLine($"No encoder was found for extension '{ext}'. Registered encoders include:"); - foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats) - { - sb.AppendFormat(" - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); - } - - throw new NotSupportedException(sb.ToString()); - } + => source.Save(path, source.DetectEncoder(path)); - IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); + /// + /// Writes the image to the given stream using the currently loaded image format. + /// + /// The source image. + /// The file path to save the image to. + /// The path is null. + /// A representing the asynchronous operation. + public static Task SaveAsync(this Image source, string path) + => source.SaveAsync(path, source.DetectEncoder(path)); - if (encoder is null) + /// + /// Writes the image to the given stream using the currently loaded image format. + /// + /// The source image. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The path is null. + /// The encoder is null. + public static void Save(this Image source, string path, IImageEncoder encoder) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(encoder, nameof(encoder)); + using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) { - var sb = new StringBuilder(); - sb.AppendLine($"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); - foreach (KeyValuePair enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) - { - sb.AppendFormat(" - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); - } - - throw new NotSupportedException(sb.ToString()); + source.Save(fs, encoder); } - - source.Save(path, encoder); } /// @@ -64,13 +61,14 @@ public static void Save(this Image source, string path) /// The encoder to save the image with. /// The path is null. /// The encoder is null. - public static void Save(this Image source, string path, IImageEncoder encoder) + /// A representing the asynchronous operation. + public static async Task SaveAsync(this Image source, string path, IImageEncoder encoder) { Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) { - source.Save(fs, encoder); + await source.SaveAsync(fs, encoder).ConfigureAwait(false); } } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index fcd38846a4..9d3abc1e82 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.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -288,6 +289,14 @@ internal override void Accept(IImageVisitor visitor) visitor.Visit(this); } + /// + internal override Task AcceptAsync(IImageVisitorAsync visitor) + { + this.EnsureNotDisposed(); + + return visitor.VisitAsync(this); + } + /// /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. /// diff --git a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs index 0ec4d500ca..6327b3eacd 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs @@ -3,8 +3,9 @@ using System; using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; // ReSharper disable InconsistentNaming @@ -91,6 +92,30 @@ public void WhenNoMatchingFormatFound_ReturnsNull() IImageFormat type = Image.DetectFormat(new Configuration(), this.DataStream); Assert.Null(type); } + + [Fact] + public async Task FromStreamAsync_GlobalConfiguration() + { + using (var stream = new MemoryStream(this.ActualImageBytes)) + { + IImageFormat type = await Image.DetectFormatAsync(new AsyncStreamWrapper(stream, () => false)); + Assert.Equal(ExpectedGlobalFormat, type); + } + } + + [Fact] + public async Task FromStreamAsync_CustomConfiguration() + { + IImageFormat type = await Image.DetectFormatAsync(this.LocalConfiguration, new AsyncStreamWrapper(this.DataStream, () => false)); + Assert.Equal(this.LocalImageFormat, type); + } + + [Fact] + public async Task WhenNoMatchingFormatFoundAsync_ReturnsNull() + { + IImageFormat type = await Image.DetectFormatAsync(new Configuration(), new AsyncStreamWrapper(this.DataStream, () => false)); + Assert.Null(type); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs index 3d1cff9784..69b1d21a6d 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; // ReSharper disable InconsistentNaming @@ -77,6 +78,17 @@ public void FromStream_GlobalConfiguration() } } + [Fact] + public void FromStream_GlobalConfiguration_NoFormat() + { + using (var stream = new MemoryStream(this.ActualImageBytes)) + { + IImageInfo info = Image.Identify(stream); + + Assert.NotNull(info); + } + } + [Fact] public void FromStream_CustomConfiguration() { @@ -86,6 +98,14 @@ public void FromStream_CustomConfiguration() Assert.Equal(this.LocalImageFormat, type); } + [Fact] + public void FromStream_CustomConfiguration_NoFormat() + { + IImageInfo info = Image.Identify(this.LocalConfiguration, this.DataStream); + + Assert.Equal(this.LocalImageInfo, info); + } + [Fact] public void WhenNoMatchingFormatFound_ReturnsNull() { @@ -94,6 +114,50 @@ public void WhenNoMatchingFormatFound_ReturnsNull() Assert.Null(info); Assert.Null(type); } + + [Fact] + public async Task FromStreamAsync_GlobalConfiguration_NoFormat() + { + using (var stream = new MemoryStream(this.ActualImageBytes)) + { + var asyncStream = new AsyncStreamWrapper(stream, () => false); + IImageInfo info = await Image.IdentifyAsync(asyncStream); + + Assert.NotNull(info); + } + } + + [Fact] + 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); + + Assert.NotNull(info.ImageInfo); + Assert.Equal(ExpectedGlobalFormat, info.Format); + } + } + + [Fact] + public async Task FromStreamAsync_CustomConfiguration() + { + var asyncStream = new AsyncStreamWrapper(this.DataStream, () => false); + (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(this.LocalConfiguration, asyncStream); + + Assert.Equal(this.LocalImageInfo, info.ImageInfo); + Assert.Equal(this.LocalImageFormat, info.Format); + } + + [Fact] + public async Task WhenNoMatchingFormatFoundAsync_ReturnsNull() + { + var asyncStream = new AsyncStreamWrapper(this.DataStream, () => false); + (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(new Configuration(), asyncStream); + + Assert.Null(info.ImageInfo); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index d0c9805cee..70d572d60a 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -60,6 +60,7 @@ 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); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs index 6cc3d6acde..1e7c7a5f57 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs @@ -3,11 +3,11 @@ using System; using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.PixelFormats; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests @@ -18,7 +18,17 @@ public class Load_FromStream_UseDefaultConfiguration : IDisposable { private static readonly byte[] Data = TestFile.Create(TestImages.Bmp.Bit8).Bytes; - private MemoryStream Stream { get; } = new MemoryStream(Data); + private MemoryStream BaseStream { get; } + + private AsyncStreamWrapper Stream { get; } + + private bool AllowSynchronousIO { get; set; } = true; + + public Load_FromStream_UseDefaultConfiguration() + { + this.BaseStream = new MemoryStream(Data); + this.Stream = new AsyncStreamWrapper(this.BaseStream, () => this.AllowSynchronousIO); + } private static void VerifyDecodedImage(Image img) { @@ -81,9 +91,73 @@ public void Stream_OutFormat_Agnostic() } } + [Fact] + public async Task Async_Stream_OutFormat_Agnostic() + { + this.AllowSynchronousIO = false; + var formattedImage = await Image.LoadWithFormatAsync(this.Stream); + using (formattedImage.Image) + { + VerifyDecodedImage(formattedImage.Image); + Assert.IsType(formattedImage.Format); + } + } + + [Fact] + public async Task Async_Stream_Specific() + { + this.AllowSynchronousIO = false; + using (var img = await Image.LoadAsync(this.Stream)) + { + VerifyDecodedImage(img); + } + } + + [Fact] + public async Task Async_Stream_Agnostic() + { + this.AllowSynchronousIO = false; + using (var img = await Image.LoadAsync(this.Stream)) + { + VerifyDecodedImage(img); + } + } + + [Fact] + public async Task Async_Stream_OutFormat_Specific() + { + this.AllowSynchronousIO = false; + var formattedImage = await Image.LoadWithFormatAsync(this.Stream); + using (formattedImage.Image) + { + VerifyDecodedImage(formattedImage.Image); + Assert.IsType(formattedImage.Format); + } + } + + [Fact] + public async Task Async_Stream_Decoder_Specific() + { + this.AllowSynchronousIO = false; + using (var img = await Image.LoadAsync(this.Stream, new BmpDecoder())) + { + VerifyDecodedImage(img); + } + } + + [Fact] + public async Task Async_Stream_Decoder_Agnostic() + { + this.AllowSynchronousIO = false; + using (var img = await Image.LoadAsync(this.Stream, new BmpDecoder())) + { + VerifyDecodedImage(img); + } + } + public void Dispose() { - this.Stream?.Dispose(); + this.BaseStream?.Dispose(); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs new file mode 100644 index 0000000000..4a6c96ae89 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using Moq; + +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests +{ + using System.Threading.Tasks; + using SixLabors.ImageSharp.Advanced; + using SixLabors.ImageSharp.Formats; + using SixLabors.ImageSharp.Tests.TestUtilities; + + public partial class ImageTests + { + public class SaveAsync + { + [Fact] + public async Task DetectedEncoding() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageTests)); + string file = Path.Combine(dir, "DetectedEncodingAsync.png"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task WhenExtensionIsUnknown_Throws() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageTests)); + string file = System.IO.Path.Combine(dir, "UnknownExtensionsEncoding_Throws.tmp"); + + await Assert.ThrowsAsync( + async () => + { + using (var image = new Image(10, 10)) + { + await image.SaveAsync(file); + } + }); + } + + [Fact] + public async Task SetEncoding() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageTests)); + string file = System.IO.Path.Combine(dir, "SetEncoding.dat"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsync(file, new PngEncoder()); + } + + using (Image.Load(file, out var mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task ThrowsWhenDisposed() + { + var image = new Image(5, 5); + image.Dispose(); + IImageEncoder encoder = Mock.Of(); + using (var stream = new MemoryStream()) + { + await Assert.ThrowsAsync(async () => await image.SaveAsync(stream, encoder)); + } + } + + [Theory] + [InlineData("test.png")] + [InlineData("test.tga")] + [InlineData("test.bmp")] + [InlineData("test.jpg")] + [InlineData("test.gif")] + public async Task SaveNeverCallsSyncMethods(string filename) + { + using (var image = new Image(5, 5)) + { + IImageEncoder encoder = image.DetectEncoder(filename); + using (var stream = new MemoryStream()) + { + var asyncStream = new AsyncStreamWrapper(stream, () => false); + await image.SaveAsync(asyncStream, encoder); + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index a609a92051..a883039f27 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using System.Numerics; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -187,7 +187,7 @@ public TestHeader(TestFormat testFormat) } } - public class TestDecoder : ImageSharp.Formats.IImageDecoder + public class TestDecoder : IImageDecoder { private TestFormat testFormat; @@ -219,9 +219,15 @@ 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 class TestEncoder : ImageSharp.Formats.IImageEncoder @@ -242,6 +248,13 @@ public void Encode(Image image, Stream stream) { // TODO record this happened so we can verify it. } + + public Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // TODO record this happened so we can verify it. + return Task.CompletedTask; + } } public struct TestPixelForAgnosticDecode : IPixel diff --git a/tests/ImageSharp.Tests/TestUtilities/AsyncStreamWrapper.cs b/tests/ImageSharp.Tests/TestUtilities/AsyncStreamWrapper.cs new file mode 100644 index 0000000000..6a05ce0bcc --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/AsyncStreamWrapper.cs @@ -0,0 +1,118 @@ +// 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 +{ + // https://github.com/dotnet/aspnetcore/blob/620c673705bb17b33cbc5ff32872d85a5fbf82b9/src/Hosting/TestHost/src/AsyncStreamWrapper.cs + internal class AsyncStreamWrapper : Stream + { + private Stream inner; + private Func allowSynchronousIO; + + internal AsyncStreamWrapper(Stream inner, Func allowSynchronousIO) + { + this.inner = inner; + this.allowSynchronousIO = allowSynchronousIO; + } + + public override bool CanRead => this.inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => this.inner.CanWrite; + + public override long Length => this.inner.Length; + + public override long Position + { + get => throw new NotSupportedException("The stream is not seekable."); + set => throw new NotSupportedException("The stream is not seekable."); + } + + public override void Flush() + { + // Not blocking Flush because things like StreamWriter.Dispose() always call it. + this.inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return this.inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!this.allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true."); + } + + return this.inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return this.inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return this.inner.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return this.inner.EndRead(asyncResult); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("The stream is not seekable."); + } + + public override void SetLength(long value) + { + throw new NotSupportedException("The stream is not seekable."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!this.allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true."); + } + + this.inner.Write(buffer, offset, count); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return this.inner.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + this.inner.EndWrite(asyncResult); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return this.inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override void Close() + { + // Don't dispose the inner stream, we don't want to impact the client stream + } + + protected override void Dispose(bool disposing) + { + // Don't dispose the inner stream, we don't want to impact the client stream + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 4594e5b1f1..4d1e754402 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -5,7 +5,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - +using System.Threading.Tasks; using ImageMagick; using SixLabors.ImageSharp.Advanced; @@ -49,6 +49,10 @@ private static void FromRgba64Bytes(Configuration configuration, Span> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + => Task.FromResult(this.Decode(configuration, stream)); + public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { @@ -80,5 +84,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); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 4971ca0374..ae6e2e3f16 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -2,7 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; +using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -13,6 +14,10 @@ public class SystemDrawingReferenceDecoder : IImageDecoder, IImageInfoDetector { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); + public Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + => Task.FromResult(this.Decode(configuration, stream)); + public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { @@ -43,6 +48,9 @@ public Image Decode(Configuration configuration, Stream stream) } } + public Task IdentifyAsync(Configuration configuration, Stream stream) + => Task.FromResult(this.Identify(configuration, stream)); + public IImageInfo Identify(Configuration configuration, Stream stream) { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) @@ -53,5 +61,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); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs index e68f53909e..2aaf7bf80c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs @@ -1,9 +1,9 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Drawing.Imaging; using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -30,5 +30,16 @@ public void Encode(Image image, Stream stream) sdBitmap.Save(stream, this.imageFormat); } } + + public Task EncodeAsync(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + using (System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) + { + sdBitmap.Save(stream, this.imageFormat); + } + + return Task.CompletedTask; + } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 439709566e..978c0555cc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Concurrent; using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -349,6 +349,9 @@ private class TestDecoder : IImageDecoder private static readonly ConcurrentDictionary InvocationCounts = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary InvocationCountsAsync = + new ConcurrentDictionary(); + private static readonly object Monitor = new object(); private string callerName; @@ -368,15 +371,27 @@ public Image Decode(Configuration configuration, Stream stream) return new Image(42, 42); } + public Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + InvocationCountsAsync[this.callerName]++; + return Task.FromResult(new Image(42, 42)); + } + internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName]; + internal static int GetInvocationCountAsync(string callerName) => InvocationCountsAsync[callerName]; + internal void InitCaller(string name) { this.callerName = name; InvocationCounts[name] = 0; + InvocationCountsAsync[name] = 0; } public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); } private class TestDecoderWithParameters : IImageDecoder @@ -384,6 +399,9 @@ private class TestDecoderWithParameters : IImageDecoder private static readonly ConcurrentDictionary InvocationCounts = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary InvocationCountsAsync = + new ConcurrentDictionary(); + private static readonly object Monitor = new object(); private string callerName; @@ -407,15 +425,27 @@ public Image Decode(Configuration configuration, Stream stream) return new Image(42, 42); } + public Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + InvocationCountsAsync[this.callerName]++; + return Task.FromResult(new Image(42, 42)); + } + internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName]; + internal static int GetInvocationCountAsync(string callerName) => InvocationCountsAsync[callerName]; + internal void InitCaller(string name) { this.callerName = name; InvocationCounts[name] = 0; + InvocationCountsAsync[name] = 0; } public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); } } }