From 4ecb74f0eda4ccc581ab612dfc8832c86b94f2a1 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 6 May 2020 10:47:44 +0100 Subject: [PATCH] implement Load Async apis --- src/ImageSharp/Image.Decode.cs | 45 ++- src/ImageSharp/Image.FromStream.cs | 341 ++++++++++++++++++ ...Load_FromStream_UseDefaultConfiguration.cs | 82 ++++- ...yncOnlyStream.cs => AsyncStreamWrapper.cs} | 0 4 files changed, 458 insertions(+), 10 deletions(-) rename tests/ImageSharp.Tests/TestUtilities/{AsyncOnlyStream.cs => AsyncStreamWrapper.cs} (100%) diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index cb6f01ce49..c6f9b82241 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; @@ -86,7 +87,6 @@ namespace SixLabors.ImageSharp : null; } -#pragma warning disable SA1008 // Opening parenthesis must be spaced correctly /// /// Decodes the image stream to the current image. /// @@ -96,8 +96,7 @@ namespace SixLabors.ImageSharp /// /// 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 FormattedImage Decode(Stream stream, Configuration config) where TPixel : unmanaged, IPixel { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); @@ -107,10 +106,32 @@ namespace SixLabors.ImageSharp } Image img = decoder.Decode(config, stream); - return (img, format); + return new FormattedImage(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 new . + /// + private static async Task> 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); + return new FormattedImage(img, format); + } + + private static FormattedImage Decode(Stream stream, Configuration config) { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); if (decoder is null) @@ -119,7 +140,19 @@ namespace SixLabors.ImageSharp } Image img = decoder.Decode(config, stream); - return (img, format); + return new FormattedImage(img, format); + } + + private static async Task 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); + return new FormattedImage(img, format); } /// diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index bcd11845bb..fa4b30d65a 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -99,6 +100,21 @@ namespace SixLabors.ImageSharp 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 format type of the decoded image. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// The . + public static Task 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 +127,18 @@ namespace SixLabors.ImageSharp /// 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. + /// The . + 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 +154,21 @@ namespace SixLabors.ImageSharp 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. + /// The . + 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. @@ -146,6 +189,26 @@ namespace SixLabors.ImageSharp 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 new .> + 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. /// @@ -159,6 +222,23 @@ namespace SixLabors.ImageSharp /// 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 new .> + public static async Task LoadAsync(Configuration configuration, Stream stream) + { + var fmt = await LoadWithFormatAsync(configuration, stream); + return fmt.Image; + } + /// /// Create a new instance of the class from the given stream. /// @@ -173,6 +253,20 @@ namespace SixLabors.ImageSharp 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 new .> + 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. /// @@ -188,6 +282,22 @@ namespace SixLabors.ImageSharp 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 format type of the decoded image. + /// The stream is null. + /// The stream is not readable. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A new .> + public static Task> LoadWithFormatAsync(Stream stream) + where TPixel : unmanaged, IPixel + => LoadWithFormatAsync(Configuration.Default, stream); + /// /// Create a new instance of the class from the given stream. /// @@ -203,6 +313,21 @@ namespace SixLabors.ImageSharp 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 new .> + 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. /// @@ -220,6 +345,23 @@ namespace SixLabors.ImageSharp 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 new .> + 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. /// @@ -272,6 +414,89 @@ namespace SixLabors.ImageSharp 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 new . + public static async Task LoadWithFormatAsync(Configuration configuration, Stream stream) + { + (Image img, IImageFormat format) data = await WithSeekableStreamAsync(configuration, stream, s => DecodeAsync(s, configuration)); + + if (data.img != 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 new . + public static async Task> LoadWithFormatAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + (Image img, IImageFormat format) data = await WithSeekableStreamAsync(configuration, stream, s => DecodeAsync(s, configuration)); + + if (data.img != 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 new . + public static async Task> LoadAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + (Image img, _) = await LoadWithFormatAsync(configuration, stream); + return img; + } + /// /// Decode a new instance of the class from the given stream. /// The pixel format is selected by the decoder. @@ -336,5 +561,121 @@ namespace SixLabors.ImageSharp 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 with out + // having to further wrap + if (stream.CanSeek) + { + if (configuration.ReadOrigin == ReadOrigin.Begin) + { + stream.Position = 0; + } + + return await action(stream); + } + + using (var memoryStream = new MemoryStream()) // should really find a nice way to use a pool for these!! + { + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + return await action(memoryStream); + } + } + } + + public readonly struct FormattedImage where TPixel : unmanaged, IPixel + { + public FormattedImage(Image image, IImageFormat format) + { + this.Image = image; + this.Format = format; + } + + public readonly Image Image { get; } + + public readonly IImageFormat Format { get; } + + + public static implicit operator (Image image, IImageFormat format)(FormattedImage value) + { + return (value.Image, value.Format); + } + + public static implicit operator FormattedImage((Image image, IImageFormat format) value) + { + return new FormattedImage(value.image, value.format); + } + + public override bool Equals(object obj) + { + return obj is FormattedImage other && + EqualityComparer>.Default.Equals(this.Image, other.Image) && + EqualityComparer.Default.Equals(this.Format, other.Format); + } + + public override int GetHashCode() + { + return HashCode.Combine(this.Image, this.Format); + } + + public void Deconstruct(out Image image, out IImageFormat format) + { + image = this.Image; + format = this.Format; + } + } + + public readonly struct FormattedImage + { + public FormattedImage(Image image, IImageFormat format) + { + this.Image = image; + this.Format = format; + } + + public readonly Image Image { get; } + + public readonly IImageFormat Format { get; } + + + public static implicit operator (Image image, IImageFormat format)(FormattedImage value) + { + return (value.Image, value.Format); + } + + public static implicit operator FormattedImage((Image image, IImageFormat format) value) + { + return new FormattedImage(value.image, value.format); + } + + public override bool Equals(object obj) + { + return obj is FormattedImage other && + EqualityComparer.Default.Equals(this.Image, other.Image) && + EqualityComparer.Default.Equals(this.Format, other.Format); + } + + public override int GetHashCode() + { + return HashCode.Combine(this.Image, this.Format); + } + + public void Deconstruct(out Image image, out IImageFormat format) + { + image = this.Image; + format = this.Format; + } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_UseDefaultConfiguration.cs index ab3a87a315..9a7960f0fd 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 @@ namespace SixLabors.ImageSharp.Tests { 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 @@ namespace SixLabors.ImageSharp.Tests } } + [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/TestUtilities/AsyncOnlyStream.cs b/tests/ImageSharp.Tests/TestUtilities/AsyncStreamWrapper.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/AsyncOnlyStream.cs rename to tests/ImageSharp.Tests/TestUtilities/AsyncStreamWrapper.cs