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