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
+ }
+ }
+}