diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs
index fdaa5c35d..a37a32717 100644
--- a/src/ImageSharp/Formats/ImageEncoder.cs
+++ b/src/ImageSharp/Formats/ImageEncoder.cs
@@ -51,7 +51,7 @@ public abstract class ImageEncoder : IImageEncoder
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
- this.Encode(image, stream, cancellationToken);
+ this.Encode(image, ms, cancellationToken);
ms.Position = 0;
ms.CopyTo(stream, configuration.StreamProcessingBufferSize);
}
diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs
index 253454814..760d1d334 100644
--- a/src/ImageSharp/IO/ChunkedMemoryStream.cs
+++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.IO;
@@ -14,42 +15,19 @@ namespace SixLabors.ImageSharp.IO;
///
internal sealed class ChunkedMemoryStream : Stream
{
- // The memory allocator.
- private readonly MemoryAllocator allocator;
-
- // Data
- private MemoryChunk? memoryChunk;
-
- // The total number of allocated chunks
- private int chunkCount;
-
- // The length of the largest contiguous buffer that can be handled by the allocator.
- private readonly int allocatorCapacity;
-
- // Has the stream been disposed.
+ private readonly MemoryChunkBuffer memoryChunkBuffer;
+ private long length;
+ private long position;
+ private int bufferIndex;
+ private int chunkIndex;
private bool isDisposed;
- // Current chunk to write to
- private MemoryChunk? writeChunk;
-
- // Offset into chunk to write to
- private int writeOffset;
-
- // Current chunk to read from
- private MemoryChunk? readChunk;
-
- // Offset into chunk to read from
- private int readOffset;
-
///
/// Initializes a new instance of the class.
///
/// The memory allocator.
public ChunkedMemoryStream(MemoryAllocator allocator)
- {
- this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
- this.allocator = allocator;
- }
+ => this.memoryChunkBuffer = new(allocator);
///
public override bool CanRead => !this.isDisposed;
@@ -66,25 +44,7 @@ internal sealed class ChunkedMemoryStream : Stream
get
{
this.EnsureNotDisposed();
-
- int length = 0;
- MemoryChunk? chunk = this.memoryChunk;
- while (chunk != null)
- {
- MemoryChunk? next = chunk.Next;
- if (next != null)
- {
- length += chunk.Length;
- }
- else
- {
- length += this.writeOffset;
- }
-
- chunk = next;
- }
-
- return length;
+ return this.length;
}
}
@@ -94,93 +54,35 @@ internal sealed class ChunkedMemoryStream : Stream
get
{
this.EnsureNotDisposed();
-
- if (this.readChunk is null)
- {
- return 0;
- }
-
- int pos = 0;
- MemoryChunk? chunk = this.memoryChunk;
- while (chunk != this.readChunk && chunk is not null)
- {
- pos += chunk.Length;
- chunk = chunk.Next;
- }
-
- pos += this.readOffset;
-
- return pos;
+ return this.position;
}
set
{
this.EnsureNotDisposed();
-
- if (value < 0)
- {
- ThrowArgumentOutOfRange(nameof(value));
- }
-
- // Back up current position in case new position is out of range
- MemoryChunk? backupReadChunk = this.readChunk;
- int backupReadOffset = this.readOffset;
-
- this.readChunk = null;
- this.readOffset = 0;
-
- int leftUntilAtPos = (int)value;
- MemoryChunk? chunk = this.memoryChunk;
- while (chunk != null)
- {
- if ((leftUntilAtPos < chunk.Length)
- || ((leftUntilAtPos == chunk.Length)
- && (chunk.Next is null)))
- {
- // The desired position is in this chunk
- this.readChunk = chunk;
- this.readOffset = leftUntilAtPos;
- break;
- }
-
- leftUntilAtPos -= chunk.Length;
- chunk = chunk.Next;
- }
-
- if (this.readChunk is null)
- {
- // Position is out of range
- this.readChunk = backupReadChunk;
- this.readOffset = backupReadOffset;
- }
+ this.SetPosition(value);
}
}
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override void Flush()
+ {
+ }
+
+ ///
public override long Seek(long offset, SeekOrigin origin)
{
this.EnsureNotDisposed();
- switch (origin)
+ this.Position = origin switch
{
- case SeekOrigin.Begin:
- this.Position = offset;
- break;
-
- case SeekOrigin.Current:
- this.Position += offset;
- break;
-
- case SeekOrigin.End:
- this.Position = this.Length + offset;
- break;
- default:
- ThrowInvalidSeek();
- break;
- }
+ SeekOrigin.Begin => (int)offset,
+ SeekOrigin.Current => (int)(this.Position + offset),
+ SeekOrigin.End => (int)(this.Length + offset),
+ _ => throw new ArgumentOutOfRangeException(nameof(offset)),
+ };
- return this.Position;
+ return this.position;
}
///
@@ -188,39 +90,13 @@ internal sealed class ChunkedMemoryStream : Stream
=> throw new NotSupportedException();
///
- protected override void Dispose(bool disposing)
- {
- if (this.isDisposed)
- {
- return;
- }
-
- try
- {
- this.isDisposed = true;
- if (disposing)
- {
- ReleaseMemoryChunks(this.memoryChunk);
- }
-
- this.memoryChunk = null;
- this.writeChunk = null;
- this.readChunk = null;
- this.chunkCount = 0;
- }
- finally
- {
- base.Dispose(disposing);
- }
- }
-
- ///
- public override void Flush()
+ public override int ReadByte()
{
+ Unsafe.SkipInit(out byte b);
+ return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1;
}
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
@@ -230,111 +106,70 @@ internal sealed class ChunkedMemoryStream : Stream
const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
- return this.ReadImpl(buffer.AsSpan(offset, count));
+ return this.Read(buffer.AsSpan(offset, count));
}
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override int Read(Span buffer) => this.ReadImpl(buffer);
-
- private int ReadImpl(Span buffer)
+ public override int Read(Span buffer)
{
this.EnsureNotDisposed();
- if (this.readChunk is null)
- {
- if (this.memoryChunk is null)
- {
- return 0;
- }
+ int offset = 0;
+ int count = buffer.Length;
- this.readChunk = this.memoryChunk;
- this.readOffset = 0;
+ long remaining = this.length - this.position;
+ if (remaining <= 0)
+ {
+ // Already at the end of the stream, nothing to read
+ return 0;
}
- IMemoryOwner chunkBuffer = this.readChunk.Buffer;
- int chunkSize = this.readChunk.Length;
- if (this.readChunk.Next is null)
+ if (remaining > count)
{
- chunkSize = this.writeOffset;
+ remaining = count;
}
+ // 'remaining' can be less than the provided buffer length.
+ int bytesToRead = (int)remaining;
int bytesRead = 0;
- int offset = 0;
- int count = buffer.Length;
- while (count > 0)
+ while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
{
- if (this.readOffset == chunkSize)
+ bool moveToNextChunk = false;
+ MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
+ int n = bytesToRead;
+ int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
+ if (n >= remainingBytesInCurrentChunk)
{
- // Exit if no more chunks are currently available
- if (this.readChunk.Next is null)
- {
- break;
- }
-
- this.readChunk = this.readChunk.Next;
- this.readOffset = 0;
- chunkBuffer = this.readChunk.Buffer;
- chunkSize = this.readChunk.Length;
- if (this.readChunk.Next is null)
- {
- chunkSize = this.writeOffset;
- }
+ n = remainingBytesInCurrentChunk;
+ moveToNextChunk = true;
}
- int readCount = Math.Min(count, chunkSize - this.readOffset);
- chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]);
- offset += readCount;
- count -= readCount;
- this.readOffset += readCount;
- bytesRead += readCount;
- }
-
- return bytesRead;
- }
-
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override int ReadByte()
- {
- this.EnsureNotDisposed();
+ // Read n bytes from the current chunk
+ chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n));
+ bytesToRead -= n;
+ offset += n;
+ bytesRead += n;
- if (this.readChunk is null)
- {
- if (this.memoryChunk is null)
+ if (moveToNextChunk)
{
- return 0;
+ this.chunkIndex = 0;
+ this.bufferIndex++;
}
-
- this.readChunk = this.memoryChunk;
- this.readOffset = 0;
- }
-
- IMemoryOwner chunkBuffer = this.readChunk.Buffer;
- int chunkSize = this.readChunk.Length;
- if (this.readChunk.Next is null)
- {
- chunkSize = this.writeOffset;
- }
-
- if (this.readOffset == chunkSize)
- {
- // Exit if no more chunks are currently available
- if (this.readChunk.Next is null)
+ else
{
- return -1;
+ this.chunkIndex += n;
}
-
- this.readChunk = this.readChunk.Next;
- this.readOffset = 0;
- chunkBuffer = this.readChunk.Buffer;
}
- return chunkBuffer.GetSpan()[this.readOffset++];
+ this.position += bytesRead;
+ return bytesRead;
}
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override void WriteByte(byte value)
+ => this.Write(MemoryMarshal.CreateSpan(ref value, 1));
+
+ ///
public override void Write(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
@@ -344,157 +179,198 @@ internal sealed class ChunkedMemoryStream : Stream
const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
- this.WriteImpl(buffer.AsSpan(offset, count));
+ this.Write(buffer.AsSpan(offset, count));
}
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer);
-
- private void WriteImpl(ReadOnlySpan buffer)
+ public override void Write(ReadOnlySpan buffer)
{
this.EnsureNotDisposed();
- if (this.memoryChunk is null)
+ int offset = 0;
+ int count = buffer.Length;
+
+ long remaining = this.memoryChunkBuffer.Length - this.position;
+
+ // Ensure we have enough capacity to write the data.
+ while (remaining < count)
{
- this.memoryChunk = this.AllocateMemoryChunk();
- this.writeChunk = this.memoryChunk;
- this.writeOffset = 0;
+ this.memoryChunkBuffer.Expand();
+ remaining = this.memoryChunkBuffer.Length - this.position;
}
- Guard.NotNull(this.writeChunk);
-
- Span chunkBuffer = this.writeChunk.Buffer.GetSpan();
- int chunkSize = this.writeChunk.Length;
- int count = buffer.Length;
- int offset = 0;
- while (count > 0)
+ int bytesToWrite = count;
+ int bytesWritten = 0;
+ while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
{
- if (this.writeOffset == chunkSize)
+ bool moveToNextChunk = false;
+ MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
+ int n = bytesToWrite;
+ int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
+ if (n >= remainingBytesInCurrentChunk)
{
- // Allocate a new chunk if the current one is full
- this.writeChunk.Next = this.AllocateMemoryChunk();
- this.writeChunk = this.writeChunk.Next;
- this.writeOffset = 0;
- chunkBuffer = this.writeChunk.Buffer.GetSpan();
- chunkSize = this.writeChunk.Length;
+ n = remainingBytesInCurrentChunk;
+ moveToNextChunk = true;
}
- int copyCount = Math.Min(count, chunkSize - this.writeOffset);
- buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]);
+ // Write n bytes to the current chunk
+ buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n));
+ bytesToWrite -= n;
+ offset += n;
+ bytesWritten += n;
- offset += copyCount;
- count -= copyCount;
- this.writeOffset += copyCount;
+ if (moveToNextChunk)
+ {
+ this.chunkIndex = 0;
+ this.bufferIndex++;
+ }
+ else
+ {
+ this.chunkIndex += n;
+ }
}
+
+ this.position += bytesWritten;
+ this.length += bytesWritten;
}
- ///
- public override void WriteByte(byte value)
+ ///
+ /// Writes the entire contents of this memory stream to another stream.
+ ///
+ /// The stream to write this memory stream to.
+ /// is .
+ /// The current or target stream is closed.
+ public void WriteTo(Stream stream)
{
+ Guard.NotNull(stream, nameof(stream));
this.EnsureNotDisposed();
- if (this.memoryChunk is null)
+ this.Position = 0;
+
+ long remaining = this.length - this.position;
+ if (remaining <= 0)
{
- this.memoryChunk = this.AllocateMemoryChunk();
- this.writeChunk = this.memoryChunk;
- this.writeOffset = 0;
+ // Already at the end of the stream, nothing to read
+ return;
}
- Guard.NotNull(this.writeChunk);
+ int bytesToRead = (int)remaining;
+ int bytesRead = 0;
+ while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
+ {
+ bool moveToNextChunk = false;
+ MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
+ int n = bytesToRead;
+ int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
+ if (n >= remainingBytesInCurrentChunk)
+ {
+ n = remainingBytesInCurrentChunk;
+ moveToNextChunk = true;
+ }
- IMemoryOwner chunkBuffer = this.writeChunk.Buffer;
- int chunkSize = this.writeChunk.Length;
+ // Read n bytes from the current chunk
+ stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n));
+ bytesToRead -= n;
+ bytesRead += n;
- if (this.writeOffset == chunkSize)
- {
- // Allocate a new chunk if the current one is full
- this.writeChunk.Next = this.AllocateMemoryChunk();
- this.writeChunk = this.writeChunk.Next;
- this.writeOffset = 0;
- chunkBuffer = this.writeChunk.Buffer;
+ if (moveToNextChunk)
+ {
+ this.chunkIndex = 0;
+ this.bufferIndex++;
+ }
+ else
+ {
+ this.chunkIndex += n;
+ }
}
- chunkBuffer.GetSpan()[this.writeOffset++] = value;
+ this.position += bytesRead;
}
///
- /// Copy entire buffer into an array.
+ /// Writes the stream contents to a byte array, regardless of the property.
///
- /// The .
+ /// A new .
public byte[] ToArray()
{
- int length = (int)this.Length; // This will throw if stream is closed
- byte[] copy = new byte[this.Length];
-
- MemoryChunk? backupReadChunk = this.readChunk;
- int backupReadOffset = this.readOffset;
-
- this.readChunk = this.memoryChunk;
- this.readOffset = 0;
- this.Read(copy, 0, length);
-
- this.readChunk = backupReadChunk;
- this.readOffset = backupReadOffset;
+ this.EnsureNotDisposed();
+ long position = this.position;
+ byte[] copy = new byte[this.length];
+ this.Position = 0;
+ _ = this.Read(copy, 0, copy.Length);
+ this.Position = position;
return copy;
}
- ///
- /// Write remainder of this stream to another stream.
- ///
- /// The stream to write to.
- public void WriteTo(Stream stream)
+ ///
+ protected override void Dispose(bool disposing)
{
- this.EnsureNotDisposed();
-
- Guard.NotNull(stream, nameof(stream));
+ if (this.isDisposed)
+ {
+ return;
+ }
- if (this.readChunk is null)
+ try
{
- if (this.memoryChunk is null)
+ this.isDisposed = true;
+ if (disposing)
{
- return;
+ this.memoryChunkBuffer.Dispose();
}
- this.readChunk = this.memoryChunk;
- this.readOffset = 0;
+ this.bufferIndex = 0;
+ this.chunkIndex = 0;
+ this.position = 0;
+ this.length = 0;
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ private void SetPosition(long value)
+ {
+ long newPosition = value;
+ if (newPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
}
- IMemoryOwner chunkBuffer = this.readChunk.Buffer;
- int chunkSize = this.readChunk.Length;
- if (this.readChunk.Next is null)
+ this.position = newPosition;
+
+ // Find the current chunk & current chunk index
+ int currentChunkIndex = 0;
+ long offset = newPosition;
+
+ // If the new position is greater than the length of the stream, set the position to the end of the stream
+ if (offset > 0 && offset >= this.memoryChunkBuffer.Length)
{
- chunkSize = this.writeOffset;
+ this.bufferIndex = this.memoryChunkBuffer.ChunkCount - 1;
+ this.chunkIndex = this.memoryChunkBuffer[this.bufferIndex].Length - 1;
+ return;
}
- // Following code mirrors Read() logic (readChunk/readOffset should
- // point just past last byte of last chunk when done)
- // loop until end of chunks is found
- while (true)
+ // Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk
+ // from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk.
+ while (offset != 0)
{
- if (this.readOffset == chunkSize)
+ int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length;
+ if (offset < chunkLength)
{
- // Exit if no more chunks are currently available
- if (this.readChunk.Next is null)
- {
- break;
- }
-
- this.readChunk = this.readChunk.Next;
- this.readOffset = 0;
- chunkBuffer = this.readChunk.Buffer;
- chunkSize = this.readChunk.Length;
- if (this.readChunk.Next is null)
- {
- chunkSize = this.writeOffset;
- }
+ // Found the correct chunk and the corresponding index
+ break;
}
- int writeCount = chunkSize - this.readOffset;
- stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount);
- this.readOffset = chunkSize;
+ offset -= chunkLength;
+ currentChunkIndex++;
}
+
+ this.bufferIndex = currentChunkIndex;
+
+ // Safe to cast here as we know the offset is less than the chunk length.
+ this.chunkIndex = (int)offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -507,48 +383,66 @@ internal sealed class ChunkedMemoryStream : Stream
}
[MethodImpl(MethodImplOptions.NoInlining)]
- private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed.");
+ private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed.");
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value);
+ private sealed class MemoryChunkBuffer : IDisposable
+ {
+ private readonly List memoryChunks = new();
+ private readonly MemoryAllocator allocator;
+ private readonly int allocatorCapacity;
+ private bool isDisposed;
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin.");
+ public MemoryChunkBuffer(MemoryAllocator allocator)
+ {
+ this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
+ this.allocator = allocator;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private MemoryChunk AllocateMemoryChunk()
- {
- // Tweak our buffer sizes to take the minimum of the provided buffer sizes
- // or the allocator buffer capacity which provides us with the largest
- // available contiguous buffer size.
- IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++)));
+ public int ChunkCount => this.memoryChunks.Count;
- return new MemoryChunk(buffer)
+ public long Length { get; private set; }
+
+ public MemoryChunk this[int index] => this.memoryChunks[index];
+
+ public void Expand()
{
- Next = null,
- Length = buffer.Length()
- };
- }
+ IMemoryOwner buffer =
+ this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount)));
- private static void ReleaseMemoryChunks(MemoryChunk? chunk)
- {
- while (chunk != null)
+ MemoryChunk chunk = new(buffer)
+ {
+ Length = buffer.Length()
+ };
+
+ this.memoryChunks.Add(chunk);
+ this.Length += chunk.Length;
+ }
+
+ public void Dispose()
{
- chunk.Dispose();
- chunk = chunk.Next;
+ if (!this.isDisposed)
+ {
+ foreach (MemoryChunk chunk in this.memoryChunks)
+ {
+ chunk.Dispose();
+ }
+
+ this.memoryChunks.Clear();
+ this.Length = 0;
+ this.isDisposed = true;
+ }
}
- }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetChunkSize(int i)
- {
- // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator.
- // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
-#pragma warning disable IDE1006 // Naming Styles
- const int _128K = 1 << 17;
- const int _4M = 1 << 22;
- return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M;
-#pragma warning restore IDE1006 // Naming Styles
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetChunkSize(int i)
+ {
+ // Increment chunks sizes with moderate speed, but without using too many buffers from the
+ // same ArrayPool bucket of the default MemoryAllocator.
+ // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
+ const int b128K = 1 << 17;
+ const int b4M = 1 << 22;
+ return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M;
+ }
}
private sealed class MemoryChunk : IDisposable
@@ -559,27 +453,15 @@ internal sealed class ChunkedMemoryStream : Stream
public IMemoryOwner Buffer { get; }
- public MemoryChunk? Next { get; set; }
-
public int Length { get; init; }
- private void Dispose(bool disposing)
+ public void Dispose()
{
if (!this.isDisposed)
{
- if (disposing)
- {
- this.Buffer.Dispose();
- }
-
+ this.Buffer.Dispose();
this.isDisposed = true;
}
}
-
- public void Dispose()
- {
- this.Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
index 072d8b854..11b1749ab 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
@@ -546,6 +546,44 @@ public class WebpEncoderTests
[Fact]
public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic);
+ [Theory]
+ [WithFile(TestPatternOpaque, PixelTypes.Rgba32)]
+ public void CanSave_NonSeekableStream(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ WebpEncoder encoder = new();
+
+ using MemoryStream seekable = new();
+ image.Save(seekable, encoder);
+
+ using MemoryStream memoryStream = new();
+ using NonSeekableStream nonSeekable = new(memoryStream);
+
+ image.Save(nonSeekable, encoder);
+
+ Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray()));
+ }
+
+ [Theory]
+ [WithFile(TestPatternOpaque, PixelTypes.Rgba32)]
+ public async Task CanSave_NonSeekableStream_Async(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ WebpEncoder encoder = new();
+
+ await using MemoryStream seekable = new();
+ image.Save(seekable, encoder);
+
+ await using MemoryStream memoryStream = new();
+ await using NonSeekableStream nonSeekable = new(memoryStream);
+
+ await image.SaveAsync(nonSeekable, encoder);
+
+ Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray()));
+ }
+
private static ImageComparer GetComparer(int quality)
{
float tolerance = 0.01f; // ~1.0%
diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
index 1803cfddb..390170cfe 100644
--- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
+++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
@@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Tests.IO;
///
public class ChunkedMemoryStreamTests
{
+ private readonly Random bufferFiller = new(123);
+
///
/// The default length in bytes of each buffer chunk when allocating large buffers.
///
@@ -30,7 +32,7 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_GetPositionTest_Negative()
{
- using var ms = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream ms = new(this.allocator);
long iCurrentPos = ms.Position;
for (int i = -1; i > -6; i--)
{
@@ -42,7 +44,7 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_ReadTest_Negative()
{
- var ms2 = new ChunkedMemoryStream(this.allocator);
+ ChunkedMemoryStream ms2 = new(this.allocator);
Assert.Throws(() => ms2.Read(null, 0, 0));
Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0));
@@ -64,7 +66,7 @@ public class ChunkedMemoryStreamTests
public void MemoryStream_ReadByteTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
- using var cms = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@@ -85,7 +87,7 @@ public class ChunkedMemoryStreamTests
public void MemoryStream_ReadByteBufferTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
- using var cms = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@@ -105,10 +107,11 @@ public class ChunkedMemoryStreamTests
[InlineData(DefaultSmallChunkSize * 4)]
[InlineData((int)(DefaultSmallChunkSize * 5.5))]
[InlineData(DefaultSmallChunkSize * 16)]
+ [InlineData(DefaultSmallChunkSize * 32)]
public void MemoryStream_ReadByteBufferSpanTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
- using var cms = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@@ -122,18 +125,24 @@ public class ChunkedMemoryStreamTests
}
}
- [Fact]
- public void MemoryStream_WriteToTests()
+ [Theory]
+ [InlineData(DefaultSmallChunkSize)]
+ [InlineData((int)(DefaultSmallChunkSize * 1.5))]
+ [InlineData(DefaultSmallChunkSize * 4)]
+ [InlineData((int)(DefaultSmallChunkSize * 5.5))]
+ [InlineData(DefaultSmallChunkSize * 16)]
+ [InlineData(DefaultSmallChunkSize * 32)]
+ public void MemoryStream_WriteToTests(int length)
{
- using (var ms2 = new ChunkedMemoryStream(this.allocator))
+ using (ChunkedMemoryStream ms2 = new(this.allocator))
{
byte[] bytArrRet;
- byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+ byte[] bytArr = this.CreateTestBuffer(length);
// [] Write to memoryStream, check the memoryStream
ms2.Write(bytArr, 0, bytArr.Length);
- using var readonlyStream = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream readonlyStream = new(this.allocator);
ms2.WriteTo(readonlyStream);
readonlyStream.Flush();
readonlyStream.Position = 0;
@@ -146,11 +155,11 @@ public class ChunkedMemoryStreamTests
}
// [] Write to memoryStream, check the memoryStream
- using (var ms2 = new ChunkedMemoryStream(this.allocator))
- using (var ms3 = new ChunkedMemoryStream(this.allocator))
+ using (ChunkedMemoryStream ms2 = new(this.allocator))
+ using (ChunkedMemoryStream ms3 = new(this.allocator))
{
byte[] bytArrRet;
- byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+ byte[] bytArr = this.CreateTestBuffer(length);
ms2.Write(bytArr, 0, bytArr.Length);
ms2.WriteTo(ms3);
@@ -164,21 +173,29 @@ public class ChunkedMemoryStreamTests
}
}
- [Fact]
- public void MemoryStream_WriteToSpanTests()
+ [Theory]
+ [InlineData(DefaultSmallChunkSize)]
+ [InlineData((int)(DefaultSmallChunkSize * 1.5))]
+ [InlineData(DefaultSmallChunkSize * 4)]
+ [InlineData((int)(DefaultSmallChunkSize * 5.5))]
+ [InlineData(DefaultSmallChunkSize * 16)]
+ [InlineData(DefaultSmallChunkSize * 32)]
+ public void MemoryStream_WriteToSpanTests(int length)
{
- using (var ms2 = new ChunkedMemoryStream(this.allocator))
+ using (ChunkedMemoryStream ms2 = new(this.allocator))
{
Span bytArrRet;
- Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+ Span bytArr = this.CreateTestBuffer(length);
// [] Write to memoryStream, check the memoryStream
ms2.Write(bytArr, 0, bytArr.Length);
- using var readonlyStream = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream readonlyStream = new(this.allocator);
ms2.WriteTo(readonlyStream);
+
readonlyStream.Flush();
readonlyStream.Position = 0;
+
bytArrRet = new byte[(int)readonlyStream.Length];
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length);
for (int i = 0; i < bytArr.Length; i++)
@@ -188,13 +205,14 @@ public class ChunkedMemoryStreamTests
}
// [] Write to memoryStream, check the memoryStream
- using (var ms2 = new ChunkedMemoryStream(this.allocator))
- using (var ms3 = new ChunkedMemoryStream(this.allocator))
+ using (ChunkedMemoryStream ms2 = new(this.allocator))
+ using (ChunkedMemoryStream ms3 = new(this.allocator))
{
Span bytArrRet;
- Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+ Span bytArr = this.CreateTestBuffer(length);
ms2.Write(bytArr, 0, bytArr.Length);
+
ms2.WriteTo(ms3);
ms3.Position = 0;
bytArrRet = new byte[(int)ms3.Length];
@@ -209,37 +227,35 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_WriteByteTests()
{
- using (var ms2 = new ChunkedMemoryStream(this.allocator))
- {
- byte[] bytArrRet;
- byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+ using ChunkedMemoryStream ms2 = new(this.allocator);
+ byte[] bytArrRet;
+ byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
- for (int i = 0; i < bytArr.Length; i++)
- {
- ms2.WriteByte(bytArr[i]);
- }
+ for (int i = 0; i < bytArr.Length; i++)
+ {
+ ms2.WriteByte(bytArr[i]);
+ }
- using var readonlyStream = new ChunkedMemoryStream(this.allocator);
- ms2.WriteTo(readonlyStream);
- readonlyStream.Flush();
- readonlyStream.Position = 0;
- bytArrRet = new byte[(int)readonlyStream.Length];
- readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length);
- for (int i = 0; i < bytArr.Length; i++)
- {
- Assert.Equal(bytArr[i], bytArrRet[i]);
- }
+ using ChunkedMemoryStream readonlyStream = new(this.allocator);
+ ms2.WriteTo(readonlyStream);
+ readonlyStream.Flush();
+ readonlyStream.Position = 0;
+ bytArrRet = new byte[(int)readonlyStream.Length];
+ readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length);
+ for (int i = 0; i < bytArr.Length; i++)
+ {
+ Assert.Equal(bytArr[i], bytArrRet[i]);
}
}
[Fact]
public void MemoryStream_WriteToTests_Negative()
{
- using var ms2 = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream ms2 = new(this.allocator);
Assert.Throws(() => ms2.WriteTo(null));
ms2.Write(new byte[] { 1 }, 0, 1);
- var readonlyStream = new MemoryStream(new byte[1028], false);
+ MemoryStream readonlyStream = new(new byte[1028], false);
Assert.Throws(() => ms2.WriteTo(readonlyStream));
readonlyStream.Dispose();
@@ -286,7 +302,7 @@ public class ChunkedMemoryStreamTests
[MemberData(nameof(CopyToData))]
public void CopyTo(Stream source, byte[] expected)
{
- using var destination = new ChunkedMemoryStream(this.allocator);
+ using ChunkedMemoryStream destination = new(this.allocator);
source.CopyTo(destination);
Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end.
Assert.Equal(expected, destination.ToArray());
@@ -297,16 +313,16 @@ public class ChunkedMemoryStreamTests
IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories)
.Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase));
- var result = new List();
+ List result = new();
foreach (string path in allImageFiles)
{
- result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length));
+ result.Add(path[TestEnvironment.InputImagesDirectoryFullPath.Length..]);
}
return result;
}
- public static IEnumerable AllTestImages = GetAllTestImages();
+ public static IEnumerable AllTestImages { get; } = GetAllTestImages();
[Theory]
[WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)]
@@ -334,40 +350,77 @@ public class ChunkedMemoryStreamTests
((TestImageProvider.FileProvider)provider).FilePath);
using FileStream fs = File.OpenRead(fullPath);
- using var nonSeekableStream = new NonSeekableStream(fs);
+ using NonSeekableStream nonSeekableStream = new(fs);
+
+ using Image actual = Image.Load(nonSeekableStream);
+
+ ImageComparer.Exact.VerifySimilarity(expected, actual);
+ expected.Dispose();
+ }
+
+ [Theory]
+ [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)]
+ public void EncoderIntegrationTest(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ return;
+ }
+
+ Image expected;
+ try
+ {
+ expected = provider.GetImage();
+ }
+ catch
+ {
+ // The image is invalid
+ return;
+ }
- var actual = Image.Load(nonSeekableStream);
+ string fullPath = Path.Combine(
+ TestEnvironment.InputImagesDirectoryFullPath,
+ ((TestImageProvider.FileProvider)provider).FilePath);
+
+ using MemoryStream ms = new();
+ using NonSeekableStream nonSeekableStream = new(ms);
+ expected.SaveAsWebp(nonSeekableStream);
+
+ using Image actual = Image.Load(nonSeekableStream);
ImageComparer.Exact.VerifySimilarity(expected, actual);
+ expected.Dispose();
}
public static IEnumerable