From 4d95e2e92a7e1d73a2d54133f097c8ed37cef0bd Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sun, 3 May 2020 14:54:32 +0100 Subject: [PATCH] Save async tests --- .../Advanced/AdvancedImageExtensions.cs | 56 ++++++++- src/ImageSharp/Advanced/IImageVisitor.cs | 16 +++ src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 20 ++- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 15 ++- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 30 +++-- .../Formats/Jpeg/JpegDecoderCore.cs | 30 +++-- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 17 ++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 30 +++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 15 ++- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 30 +++-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 15 ++- src/ImageSharp/Image.cs | 33 ++++- src/ImageSharp/ImageExtensions.cs | 57 ++++----- src/ImageSharp/Image{TPixel}.cs | 9 ++ .../Image/ImageTests.SaveAsync.cs | 109 ++++++++++++++++ .../TestUtilities/AsyncOnlyStream.cs | 117 ++++++++++++++++++ 16 files changed, 511 insertions(+), 88 deletions(-) create mode 100644 tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/AsyncOnlyStream.cs diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index f4e9f3042..c845cfc4f 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -2,9 +2,13 @@ // Licensed under the GNU Affero General Public License, Version 3. using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; - +using System.Text; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -15,6 +19,47 @@ namespace SixLabors.ImageSharp.Advanced /// public static class AdvancedImageExtensions { + /// + /// For a given path find the best encoder to use + /// + /// The source. + /// The Path + /// The matching encoder. + public static IImageEncoder FindEncoded(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()); + } + + 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 @@ -24,6 +69,15 @@ namespace SixLabors.ImageSharp.Advanced 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. + /// The visitor. + 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 50e6337e5..fa7b8e2f1 100644 --- a/src/ImageSharp/Advanced/IImageVisitor.cs +++ b/src/ImageSharp/Advanced/IImageVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the GNU Affero General Public License, Version 3. +using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Advanced @@ -19,4 +20,19 @@ namespace SixLabors.ImageSharp.Advanced 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. + Task VisitAsync(Image image) + where TPixel : unmanaged, IPixel; + } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index b5ae055a9..7db6fea26 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -134,13 +134,21 @@ namespace SixLabors.ImageSharp.Formats.Bmp public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel { - // 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()) + // if we can seek then we arn't in a context that errors on async operations + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Decode(ms); + 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); + } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 3e10eedbb..93727bb6e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -100,11 +100,18 @@ namespace SixLabors.ImageSharp.Formats.Bmp public async Task EncodeAsync(Image image, Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); + this.Encode(image, stream); + } + else + { + using (var ms = new MemoryStream()) + { + this.Encode(image, ms); + ms.Position = 0; + await ms.CopyToAsync(stream).ConfigureAwait(false); + } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index fdac0e2ae..c79d006df 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -106,11 +106,18 @@ namespace SixLabors.ImageSharp.Formats.Gif public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Decode(ms); + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } } } @@ -186,11 +193,18 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The containing image data. public async Task IdentifyAsync(Stream stream) { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Identify(ms); + return this.Identify(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 2f9495267..b694c02bb 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -222,11 +222,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Decode(ms); + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } } } @@ -253,11 +260,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The containing image data. public async Task IdentifyAsync(Stream stream) { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Identify(ms); + return this.Identify(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index e87f9ce75..1838b8d6d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -48,12 +48,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { var encoder = new JpegEncoderCore(this); - // this hack has to be be here because JpegEncoderCore is unsafe - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - encoder.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); + 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/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f610f5750..713e5c651 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -152,11 +152,18 @@ namespace SixLabors.ImageSharp.Formats.Png public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Decode(ms); + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } } } @@ -269,11 +276,18 @@ namespace SixLabors.ImageSharp.Formats.Png /// The containing image data. public async Task IdentifyAsync(Stream stream) { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Identify(ms); + return this.Identify(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1c8696fc1..a3b7ab23d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -135,11 +135,18 @@ namespace SixLabors.ImageSharp.Formats.Png public async Task EncodeAsync(Image image, Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); + this.Encode(image, stream); + } + else + { + using (var ms = new MemoryStream()) + { + this.Encode(image, ms); + ms.Position = 0; + await ms.CopyToAsync(stream).ConfigureAwait(false); + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 808139e59..f70d7ca24 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -91,11 +91,18 @@ namespace SixLabors.ImageSharp.Formats.Tga public async Task> DecodeAsync(Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Decode(ms); + return this.Decode(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Decode(ms); + } } } @@ -676,11 +683,18 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The containing image data. public async Task IdentifyAsync(Stream stream) { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return this.Identify(ms); + return this.Identify(stream); + } + else + { + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return this.Identify(ms); + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 3b16048f3..c0da8d40b 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -64,11 +64,18 @@ namespace SixLabors.ImageSharp.Formats.Tga public async Task EncodeAsync(Image image, Stream stream) where TPixel : unmanaged, IPixel { - using (var ms = new MemoryStream()) + if (stream.CanSeek) { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); + this.Encode(image, stream); + } + else + { + using (var ms = new MemoryStream()) + { + this.Encode(image, ms); + ms.Position = 0; + await ms.CopyToAsync(stream).ConfigureAwait(false); + } } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index c43a20842..5de580283 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,21 @@ namespace SixLabors.ImageSharp 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. + public Task SaveAsync(Stream stream, IImageEncoder encoder) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(encoder, nameof(encoder)); + this.EnsureNotDisposed(); + + return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)); + } + /// /// Returns a copy of the image in the given pixel format. /// @@ -140,7 +155,15 @@ namespace SixLabors.ImageSharp /// 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; @@ -157,6 +180,12 @@ namespace SixLabors.ImageSharp { this.encoder.Encode(image, this.stream); } + + public Task VisitAsync(Image image) + where TPixel : unmanaged, IPixel + { + return this.encoder.EncodeAsync(image, this.stream); + } } } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index aa9030c6e..a71cc4064 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -22,40 +23,36 @@ namespace SixLabors.ImageSharp /// 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)); + => source.Save(path, source.FindEncoded(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()); - } - - 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. + public static Task SaveAsync(this Image source, string path) + => source.SaveAsync(path, source.FindEncoded(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); } + /// /// Writes the image to the given stream using the currently loaded image format. /// @@ -64,13 +61,13 @@ namespace SixLabors.ImageSharp /// 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) + 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 7eda2050a..64aa8ee0b 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -5,6 +5,7 @@ using System; 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 @@ namespace SixLabors.ImageSharp 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.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs new file mode 100644 index 000000000..0f87df7b2 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the GNU Affero General Public License, Version 3. + +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.Runtime.CompilerServices; + 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 = System.IO.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.FindEncoded(filename); + using (var stream = new MemoryStream()) + { + var asyncStream = new AsyncStreamWrapper(stream, () => false); + await image.SaveAsync(asyncStream, encoder); + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/AsyncOnlyStream.cs b/tests/ImageSharp.Tests/TestUtilities/AsyncOnlyStream.cs new file mode 100644 index 000000000..dc4133fbf --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/AsyncOnlyStream.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +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 => throw new NotSupportedException("The stream is not seekable."); + + 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 + } + } +}