diff --git a/src/ImageSharp/IO/IFileSystem.cs b/src/ImageSharp/IO/IFileSystem.cs
index 96a9b5ba01..0f5113eff4 100644
--- a/src/ImageSharp/IO/IFileSystem.cs
+++ b/src/ImageSharp/IO/IFileSystem.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
internal interface IFileSystem
{
///
- /// Returns a readable stream as defined by the path.
+ /// Opens a file as defined by the path and returns it as a readable stream.
///
/// Path to the file to open.
- /// A stream representing the file to open.
+ /// A stream representing the opened file.
Stream OpenRead(string path);
///
- /// Creates or opens a file and returns it as a writable stream as defined by the path.
+ /// Opens a file as defined by the path and returns it as a readable stream
+ /// that can be used for asynchronous reading.
///
/// Path to the file to open.
- /// A stream representing the file to open.
+ /// A stream representing the opened file.
+ Stream OpenReadAsynchronous(string path);
+
+ ///
+ /// Creates or opens a file as defined by the path and returns it as a writable stream.
+ ///
+ /// Path to the file to open.
+ /// A stream representing the opened file.
Stream Create(string path);
+
+ ///
+ /// Creates or opens a file as defined by the path and returns it as a writable stream
+ /// that can be used for asynchronous reading and writing.
+ ///
+ /// Path to the file to open.
+ /// A stream representing the opened file.
+ Stream CreateAsynchronous(string path);
}
diff --git a/src/ImageSharp/IO/LocalFileSystem.cs b/src/ImageSharp/IO/LocalFileSystem.cs
index f4dfa2fe14..d1f619f486 100644
--- a/src/ImageSharp/IO/LocalFileSystem.cs
+++ b/src/ImageSharp/IO/LocalFileSystem.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
///
public Stream OpenRead(string path) => File.OpenRead(path);
+ ///
+ public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
+ {
+ Mode = FileMode.Open,
+ Access = FileAccess.Read,
+ Share = FileShare.Read,
+ Options = FileOptions.Asynchronous,
+ });
+
///
public Stream Create(string path) => File.Create(path);
+
+ ///
+ public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.ReadWrite,
+ Share = FileShare.None,
+ Options = FileOptions.Asynchronous,
+ });
}
diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs
index 884acf7a40..a20e5d6c58 100644
--- a/src/ImageSharp/Image.FromFile.cs
+++ b/src/ImageSharp/Image.FromFile.cs
@@ -72,7 +72,7 @@ public static async Task DetectFormatAsync(
{
Guard.NotNull(options, nameof(options));
- using Stream stream = options.Configuration.FileSystem.OpenRead(path);
+ await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@@ -144,7 +144,7 @@ public static async Task IdentifyAsync(
CancellationToken cancellationToken = default)
{
Guard.NotNull(options, nameof(options));
- using Stream stream = options.Configuration.FileSystem.OpenRead(path);
+ await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@@ -214,7 +214,7 @@ public static async Task LoadAsync(
string path,
CancellationToken cancellationToken = default)
{
- using Stream stream = options.Configuration.FileSystem.OpenRead(path);
+ await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@@ -291,7 +291,7 @@ public static async Task> LoadAsync(
Guard.NotNull(options, nameof(options));
Guard.NotNull(path, nameof(path));
- using Stream stream = options.Configuration.FileSystem.OpenRead(path);
+ await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs
index cf970b3166..75e4f13257 100644
--- a/src/ImageSharp/ImageExtensions.cs
+++ b/src/ImageSharp/ImageExtensions.cs
@@ -70,7 +70,7 @@ public static async Task SaveAsync(
Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder));
- using Stream fs = source.GetConfiguration().FileSystem.Create(path);
+ await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
}
diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs
index 3d512b7d27..a1eeb25976 100644
--- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs
+++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
@@ -11,36 +11,113 @@ public class LocalFileSystemTests
public void OpenRead()
{
string path = Path.GetTempFileName();
- string testData = Guid.NewGuid().ToString();
- File.WriteAllText(path, testData);
+ try
+ {
+ string testData = Guid.NewGuid().ToString();
+ File.WriteAllText(path, testData);
- var fs = new LocalFileSystem();
+ LocalFileSystem fs = new();
- using (var r = new StreamReader(fs.OpenRead(path)))
- {
- string data = r.ReadToEnd();
+ using (FileStream stream = (FileStream)fs.OpenRead(path))
+ using (StreamReader reader = new(stream))
+ {
+ Assert.False(stream.IsAsync);
+ Assert.True(stream.CanRead);
+ Assert.False(stream.CanWrite);
- Assert.Equal(testData, data);
+ string data = reader.ReadToEnd();
+
+ Assert.Equal(testData, data);
+ }
}
+ finally
+ {
+ File.Delete(path);
+ }
+ }
- File.Delete(path);
+ [Fact]
+ public async Task OpenReadAsynchronous()
+ {
+ string path = Path.GetTempFileName();
+ try
+ {
+ string testData = Guid.NewGuid().ToString();
+ File.WriteAllText(path, testData);
+
+ LocalFileSystem fs = new();
+
+ await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path))
+ using (StreamReader reader = new(stream))
+ {
+ Assert.True(stream.IsAsync);
+ Assert.True(stream.CanRead);
+ Assert.False(stream.CanWrite);
+
+ string data = await reader.ReadToEndAsync();
+
+ Assert.Equal(testData, data);
+ }
+ }
+ finally
+ {
+ File.Delete(path);
+ }
}
[Fact]
public void Create()
{
string path = Path.GetTempFileName();
- string testData = Guid.NewGuid().ToString();
- var fs = new LocalFileSystem();
+ try
+ {
+ string testData = Guid.NewGuid().ToString();
+ LocalFileSystem fs = new();
+
+ using (FileStream stream = (FileStream)fs.Create(path))
+ using (StreamWriter writer = new(stream))
+ {
+ Assert.False(stream.IsAsync);
+ Assert.True(stream.CanRead);
+ Assert.True(stream.CanWrite);
- using (var r = new StreamWriter(fs.Create(path)))
+ writer.Write(testData);
+ }
+
+ string data = File.ReadAllText(path);
+ Assert.Equal(testData, data);
+ }
+ finally
{
- r.Write(testData);
+ File.Delete(path);
}
+ }
- string data = File.ReadAllText(path);
- Assert.Equal(testData, data);
+ [Fact]
+ public async Task CreateAsynchronous()
+ {
+ string path = Path.GetTempFileName();
+ try
+ {
+ string testData = Guid.NewGuid().ToString();
+ LocalFileSystem fs = new();
+
+ await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path))
+ await using (StreamWriter writer = new(stream))
+ {
+ Assert.True(stream.IsAsync);
+ Assert.True(stream.CanRead);
+ Assert.True(stream.CanWrite);
+
+ await writer.WriteAsync(testData);
+ }
- File.Delete(path);
+ string data = File.ReadAllText(path);
+ Assert.Equal(testData, data);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs
index a3f03bed5a..f9c01ab564 100644
--- a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs
+++ b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs
@@ -44,7 +44,7 @@ public ImageSaveTests()
[Fact]
public void SavePath()
{
- var stream = new MemoryStream();
+ using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream);
this.image.Save("path.png");
@@ -54,7 +54,7 @@ public void SavePath()
[Fact]
public void SavePathWithEncoder()
{
- var stream = new MemoryStream();
+ using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream);
this.image.Save("path.jpg", this.encoderNotInFormat.Object);
@@ -73,7 +73,7 @@ public void ToBase64String()
[Fact]
public void SaveStreamWithMime()
{
- var stream = new MemoryStream();
+ using MemoryStream stream = new();
this.image.Save(stream, this.localImageFormat.Object);
this.encoder.Verify(x => x.Encode(this.image, stream));
@@ -82,7 +82,7 @@ public void SaveStreamWithMime()
[Fact]
public void SaveStreamWithEncoder()
{
- var stream = new MemoryStream();
+ using MemoryStream stream = new();
this.image.Save(stream, this.encoderNotInFormat.Object);
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs
index a8f9981b44..996310d8c3 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs
@@ -122,6 +122,7 @@ protected ImageLoadTestBase()
Stream StreamFactory() => this.DataStream;
this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory);
+ this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory);
this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory);
this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object;
this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem;
@@ -132,6 +133,11 @@ public void Dispose()
// Clean up the global object;
this.localStreamReturnImageRgba32?.Dispose();
this.localStreamReturnImageAgnostic?.Dispose();
+
+ if (this.dataStreamLazy.IsValueCreated)
+ {
+ this.dataStreamLazy.Value.Dispose();
+ }
}
protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker);
diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs
index 8aefbe320e..9013d15530 100644
--- a/tests/ImageSharp.Tests/TestFileSystem.cs
+++ b/tests/ImageSharp.Tests/TestFileSystem.cs
@@ -1,6 +1,8 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+#nullable enable
+
namespace SixLabors.ImageSharp.Tests;
///
@@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests;
///
public class TestFileSystem : ImageSharp.IO.IFileSystem
{
- private readonly Dictionary> fileSystem = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary> fileSystem = new(StringComparer.OrdinalIgnoreCase);
public void AddFile(string path, Func data)
{
@@ -18,35 +20,39 @@ public void AddFile(string path, Func data)
}
}
- public Stream Create(string path)
+ public Stream Create(string path) => this.GetStream(path) ?? File.Create(path);
+
+ public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
{
- // if we have injected a fake file use it instead
- lock (this.fileSystem)
- {
- if (this.fileSystem.ContainsKey(path))
- {
- Stream stream = this.fileSystem[path]();
- stream.Position = 0;
- return stream;
- }
- }
+ Mode = FileMode.Create,
+ Access = FileAccess.ReadWrite,
+ Share = FileShare.None,
+ Options = FileOptions.Asynchronous,
+ });
- return File.Create(path);
- }
+ public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path);
+
+ public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
+ {
+ Mode = FileMode.Open,
+ Access = FileAccess.Read,
+ Share = FileShare.Read,
+ Options = FileOptions.Asynchronous,
+ });
- public Stream OpenRead(string path)
+ private Stream? GetStream(string path)
{
// if we have injected a fake file use it instead
lock (this.fileSystem)
{
- if (this.fileSystem.ContainsKey(path))
+ if (this.fileSystem.TryGetValue(path, out Func? streamFactory))
{
- Stream stream = this.fileSystem[path]();
+ Stream stream = streamFactory();
stream.Position = 0;
return stream;
}
}
- return File.OpenRead(path);
+ return null;
}
}
diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
index 732948b8e0..7b519531ab 100644
--- a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
@@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem
Stream IFileSystem.Create(string path) => this.stream;
+ Stream IFileSystem.CreateAsynchronous(string path) => this.stream;
+
Stream IFileSystem.OpenRead(string path) => this.stream;
+
+ Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream;
}