diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
index 5c0902f052..7caaa5868d 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
@@ -640,7 +640,7 @@ internal static partial class SimdUtils
/// The mask vector.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Vector128 BlendVariable(in Vector128 left, in Vector128 right, in Vector128 mask)
+ public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask)
{
if (Sse41.IsSupported)
{
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index c861e2c961..45819b751a 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -291,20 +291,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
- // TODO: Consider an optimization that trims down the buffer to the minimum size required.
- // We would use a process similar to entropy crop where we trim the buffer from the edges
- // until we hit a non-transparent pixel.
- this.WriteImageDescriptor(frame, useLocal, stream);
+ // Assign the correct buffer to compress.
+ // If we are using a local palette or it's the first run then we want to use the quantized frame.
+ Buffer2D buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
+
+ // Trim down the buffer to the minimum size required.
+ Buffer2DRegion region = TrimTransparentPixels(buffer, transparencyIndex);
+ this.WriteImageDescriptor(region.Rectangle, useLocal, stream);
if (useLocal)
{
this.WriteColorTable(quantized, stream);
}
- // Assign the correct buffer to compress.
- // If we are using a local palette or it's the first run then we want to use the quantized frame.
- Buffer2D buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
- this.WriteImageData(buffer, stream);
+ this.WriteImageData(region, stream);
// Swap the buffers.
(quantized, previousQuantized) = (previousQuantized, quantized);
@@ -386,6 +386,44 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
+ private static Buffer2DRegion TrimTransparentPixels(Buffer2D buffer, int transparencyIndex)
+ {
+ if (transparencyIndex < 0)
+ {
+ return buffer.GetRegion();
+ }
+
+ byte trimmableIndex = unchecked((byte)transparencyIndex);
+
+ int top = int.MaxValue;
+ int bottom = int.MinValue;
+ int left = int.MaxValue;
+ int right = int.MinValue;
+
+ for (int y = 0; y < buffer.Height; y++)
+ {
+ Span rowSpan = buffer.DangerousGetRowSpan(y);
+ for (int x = 0; x < rowSpan.Length; x++)
+ {
+ if (rowSpan[x] != trimmableIndex)
+ {
+ top = Math.Min(top, y);
+ bottom = Math.Max(bottom, y);
+ left = Math.Min(left, x);
+ right = Math.Max(right, x);
+ }
+ }
+ }
+
+ if (top == int.MaxValue || bottom == int.MinValue)
+ {
+ // No valid rectangle found
+ return buffer.GetRegion();
+ }
+
+ return buffer.GetRegion(Rectangle.FromLTRB(left, top, right, bottom));
+ }
+
///
/// Returns the index of the most transparent color in the palette.
///
@@ -583,7 +621,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (metadata is null)
{
metadata = new();
- hasTransparency = transparencyIndex > -1;
+ hasTransparency = transparencyIndex >= 0;
}
else
{
@@ -619,7 +657,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
IMemoryOwner? owner = null;
- Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
+ Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
if (extensionSize > 128)
{
owner = this.memoryAllocator.Allocate(extensionSize + 3);
@@ -642,14 +680,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
///
- /// Writes the image descriptor to the stream.
+ /// Writes the image frame descriptor to the stream.
///
- /// The pixel format.
- /// The to be encoded.
+ /// The frame location and size.
/// Whether to use the global color table.
/// The stream to write to.
- private void WriteImageDescriptor(ImageFrame image, bool hasColorTable, Stream stream)
- where TPixel : unmanaged, IPixel
+ private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream)
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
@@ -658,10 +694,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
localColorTableSize: this.bitDepth - 1);
GifImageDescriptor descriptor = new(
- left: 0,
- top: 0,
- width: (ushort)image.Width,
- height: (ushort)image.Height,
+ left: (ushort)rectangle.X,
+ top: (ushort)rectangle.Y,
+ width: (ushort)rectangle.Width,
+ height: (ushort)rectangle.Height,
packed: packedValue);
Span buffer = stackalloc byte[20];
@@ -697,9 +733,9 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// Writes the image pixel data to the stream.
///
- /// The containing indexed pixels.
+ /// The containing indexed pixels.
/// The stream to write to.
- private void WriteImageData(Buffer2D indices, Stream stream)
+ private void WriteImageData(Buffer2DRegion indices, Stream stream)
{
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(indices, stream);
diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index 5253c0978a..4b40c44e45 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
///
/// The 2D buffer of indexed pixels.
/// The stream to write to.
- public void Encode(Buffer2D indexedPixels, Stream stream)
+ public void Encode(Buffer2DRegion indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable
/// The 2D buffer of indexed pixels.
/// The initial bits.
/// The stream to write to.
- private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream)
+ private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;
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 abstract partial class Image
{
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 abstract partial class Image
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 abstract partial class Image
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 abstract partial class Image
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 partial class ImageExtensions
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 class ImageSaveTests : IDisposable
[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 class ImageSaveTests : IDisposable
[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 class ImageSaveTests : IDisposable
[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 class ImageSaveTests : IDisposable
[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 @@ public partial class ImageTests
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 partial class ImageTests
// 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 class TestFileSystem : ImageSharp.IO.IFileSystem
}
}
- 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;
}