diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs
new file mode 100644
index 0000000000..2d895245c7
--- /dev/null
+++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs
@@ -0,0 +1,570 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.IO
+{
+ ///
+ /// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized.
+ /// Chunks are allocated by the assigned via the constructor
+ /// and is designed to take advantage of buffer pooling when available.
+ ///
+ internal sealed class ChunkedMemoryStream : Stream
+ {
+ ///
+ /// The default length in bytes of each buffer chunk.
+ ///
+ public const int DefaultBufferLength = 81920;
+
+ // The memory allocator.
+ private readonly MemoryAllocator allocator;
+
+ // Data
+ private MemoryChunk memoryChunk;
+
+ // The length of each buffer chunk
+ private readonly int chunkLength;
+
+ // Has the stream been disposed.
+ 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.
+ ///
+ public ChunkedMemoryStream(MemoryAllocator allocator)
+ : this(DefaultBufferLength, allocator)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The length, in bytes of each buffer chunk.
+ /// The memory allocator.
+ public ChunkedMemoryStream(int bufferLength, MemoryAllocator allocator)
+ {
+ Guard.MustBeGreaterThan(bufferLength, 0, nameof(bufferLength));
+ Guard.NotNull(allocator, nameof(allocator));
+
+ this.chunkLength = bufferLength;
+ this.allocator = allocator;
+ }
+
+ ///
+ public override bool CanRead => !this.isDisposed;
+
+ ///
+ public override bool CanSeek => !this.isDisposed;
+
+ ///
+ public override bool CanWrite => !this.isDisposed;
+
+ ///
+ public override long Length
+ {
+ 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;
+ }
+ }
+
+ ///
+ public override long Position
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+
+ if (this.readChunk is null)
+ {
+ return 0;
+ }
+
+ int pos = 0;
+ MemoryChunk chunk = this.memoryChunk;
+ while (chunk != this.readChunk)
+ {
+ pos += chunk.Length;
+ chunk = chunk.Next;
+ }
+
+ pos += this.readOffset;
+
+ return pos;
+ }
+
+ 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;
+ ThrowArgumentOutOfRange(nameof(value));
+ }
+ }
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ this.EnsureNotDisposed();
+
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ this.Position = offset;
+ break;
+
+ case SeekOrigin.Current:
+ this.Position += offset;
+ break;
+
+ case SeekOrigin.End:
+ this.Position = this.Length + offset;
+ break;
+ }
+
+ return this.Position;
+ }
+
+ ///
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (this.isDisposed)
+ {
+ return;
+ }
+
+ try
+ {
+ this.isDisposed = true;
+ if (disposing)
+ {
+ this.ReleaseMemoryChunks(this.memoryChunk);
+ }
+
+ this.memoryChunk = null;
+ this.writeChunk = null;
+ this.readChunk = null;
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ ///
+ public override void Flush()
+ {
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ Guard.NotNull(buffer, nameof(buffer));
+ Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset));
+ Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count));
+ Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), $"{offset} subtracted from the buffer length is less than {count}");
+
+ return this.ReadImpl(buffer.AsSpan().Slice(offset, count));
+ }
+
+#if SUPPORTS_SPAN_STREAM
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int Read(Span buffer) => this.ReadImpl(buffer);
+#endif
+
+ private int ReadImpl(Span buffer)
+ {
+ this.EnsureNotDisposed();
+
+ if (this.readChunk is null)
+ {
+ if (this.memoryChunk is null)
+ {
+ return 0;
+ }
+
+ this.readChunk = this.memoryChunk;
+ this.readOffset = 0;
+ }
+
+ Span chunkBuffer = this.readChunk.Buffer.GetSpan();
+ int chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+
+ int bytesRead = 0;
+ int offset = 0;
+ int count = buffer.Length;
+ while (count > 0)
+ {
+ if (this.readOffset == chunkSize)
+ {
+ // 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.GetSpan();
+ chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+ }
+
+ int readCount = Math.Min(count, chunkSize - this.readOffset);
+ chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer.Slice(offset));
+ offset += readCount;
+ count -= readCount;
+ this.readOffset += readCount;
+ bytesRead += readCount;
+ }
+
+ return bytesRead;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int ReadByte()
+ {
+ this.EnsureNotDisposed();
+
+ if (this.readChunk is null)
+ {
+ if (this.memoryChunk is null)
+ {
+ return 0;
+ }
+
+ this.readChunk = this.memoryChunk;
+ this.readOffset = 0;
+ }
+
+ byte[] chunkBuffer = this.readChunk.Buffer.Array;
+ 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)
+ {
+ return -1;
+ }
+
+ this.readChunk = this.readChunk.Next;
+ this.readOffset = 0;
+ chunkBuffer = this.readChunk.Buffer.Array;
+ }
+
+ return chunkBuffer[this.readOffset++];
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override void Write(byte[] buffer, int offset, int count)
+ => this.WriteImpl(buffer.AsSpan().Slice(offset, count));
+
+#if SUPPORTS_SPAN_STREAM
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer);
+#endif
+
+ private void WriteImpl(ReadOnlySpan buffer)
+ {
+ this.EnsureNotDisposed();
+
+ if (this.memoryChunk is null)
+ {
+ this.memoryChunk = this.AllocateMemoryChunk();
+ this.writeChunk = this.memoryChunk;
+ this.writeOffset = 0;
+ }
+
+ Span chunkBuffer = this.writeChunk.Buffer.GetSpan();
+ int chunkSize = this.writeChunk.Length;
+ int count = buffer.Length;
+ int offset = 0;
+ while (count > 0)
+ {
+ 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.GetSpan();
+ chunkSize = this.writeChunk.Length;
+ }
+
+ int copyCount = Math.Min(count, chunkSize - this.writeOffset);
+ buffer.Slice(offset, copyCount).CopyTo(chunkBuffer.Slice(this.writeOffset));
+
+ offset += copyCount;
+ count -= copyCount;
+ this.writeOffset += copyCount;
+ }
+ }
+
+ ///
+ public override void WriteByte(byte value)
+ {
+ this.EnsureNotDisposed();
+
+ if (this.memoryChunk is null)
+ {
+ this.memoryChunk = this.AllocateMemoryChunk();
+ this.writeChunk = this.memoryChunk;
+ this.writeOffset = 0;
+ }
+
+ byte[] chunkBuffer = this.writeChunk.Buffer.Array;
+ int chunkSize = this.writeChunk.Length;
+
+ 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.Array;
+ }
+
+ chunkBuffer[this.writeOffset++] = value;
+ }
+
+ ///
+ /// Copy entire buffer into an array.
+ ///
+ /// The .
+ 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;
+
+ return copy;
+ }
+
+ ///
+ /// Write remainder of this stream to another stream.
+ ///
+ /// The stream to write to.
+ public void WriteTo(Stream stream)
+ {
+ this.EnsureNotDisposed();
+
+ Guard.NotNull(stream, nameof(stream));
+
+ if (this.readChunk is null)
+ {
+ if (this.memoryChunk is null)
+ {
+ return;
+ }
+
+ this.readChunk = this.memoryChunk;
+ this.readOffset = 0;
+ }
+
+ byte[] chunkBuffer = this.readChunk.Buffer.Array;
+ int chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+
+ // 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)
+ {
+ if (this.readOffset == chunkSize)
+ {
+ // 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.Array;
+ chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+ }
+
+ int writeCount = chunkSize - this.readOffset;
+ stream.Write(chunkBuffer, this.readOffset, writeCount);
+ this.readOffset = chunkSize;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void EnsureNotDisposed()
+ {
+ if (this.isDisposed)
+ {
+ ThrowDisposed();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowDisposed()
+ => throw new ObjectDisposedException(null, "The stream is closed.");
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowArgumentOutOfRange(string value)
+ => throw new ArgumentOutOfRangeException(value);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private MemoryChunk AllocateMemoryChunk()
+ {
+ IManagedByteBuffer buffer = this.allocator.AllocateManagedByteBuffer(this.chunkLength);
+ return new MemoryChunk
+ {
+ Buffer = buffer,
+ Next = null,
+ Length = buffer.Length()
+ };
+ }
+
+ private void ReleaseMemoryChunks(MemoryChunk chunk)
+ {
+ while (chunk != null)
+ {
+ chunk.Dispose();
+ chunk = chunk.Next;
+ }
+ }
+
+ private sealed class MemoryChunk : IDisposable
+ {
+ private bool isDisposed;
+
+ public IManagedByteBuffer Buffer { get; set; }
+
+ public MemoryChunk Next { get; set; }
+
+ public int Length { get; set; }
+
+ private void Dispose(bool disposing)
+ {
+ if (!this.isDisposed)
+ {
+ if (disposing)
+ {
+ this.Buffer.Dispose();
+ }
+
+ this.Buffer = null;
+ this.isDisposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ this.Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs b/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs
deleted file mode 100644
index 74864d45e6..0000000000
--- a/src/ImageSharp/IO/FixedCapacityPooledMemoryStream.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.Buffers;
-using System.IO;
-using SixLabors.ImageSharp.Memory;
-
-namespace SixLabors.ImageSharp.IO
-{
- ///
- /// A memory stream constructed from a pooled buffer of known length.
- ///
- internal sealed class FixedCapacityPooledMemoryStream : MemoryStream
- {
- private readonly IManagedByteBuffer buffer;
- private bool isDisposed;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The length of the stream buffer to rent.
- /// The allocator to rent the buffer from.
- public FixedCapacityPooledMemoryStream(long length, MemoryAllocator allocator)
- : this(RentBuffer(length, allocator)) => this.Length = length;
-
- private FixedCapacityPooledMemoryStream(IManagedByteBuffer buffer)
- : base(buffer.Array) => this.buffer = buffer;
-
- ///
- public override long Length { get; }
-
- ///
- public override bool TryGetBuffer(out ArraySegment buffer)
- {
- if (this.isDisposed)
- {
- throw new ObjectDisposedException(this.GetType().Name);
- }
-
- buffer = new ArraySegment(this.buffer.Array, 0, this.buffer.Length());
- return true;
- }
-
- ///
- protected override void Dispose(bool disposing)
- {
- if (!this.isDisposed)
- {
- this.isDisposed = true;
-
- if (disposing)
- {
- this.buffer.Dispose();
- }
-
- base.Dispose(disposing);
- }
- }
-
- // In the extrememly unlikely event someone ever gives us a stream
- // with length longer than int.MaxValue then we'll use something else.
- private static IManagedByteBuffer RentBuffer(long length, MemoryAllocator allocator)
- {
- Guard.MustBeBetweenOrEqualTo(length, 0, int.MaxValue, nameof(length));
- return allocator.AllocateManagedByteBuffer((int)length);
- }
- }
-}
diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs
index ee148cd254..b57fa9a6ca 100644
--- a/src/ImageSharp/Image.FromStream.cs
+++ b/src/ImageSharp/Image.FromStream.cs
@@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -731,7 +732,7 @@ private static T WithSeekableStream(
}
// We want to be able to load images from things like HttpContext.Request.Body
- using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
+ using var memoryStream = new ChunkedMemoryStream(configuration.MemoryAllocator);
stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize);
memoryStream.Position = 0;
@@ -775,7 +776,7 @@ private static async Task WithSeekableStreamAsync(
return await action(stream, cancellationToken).ConfigureAwait(false);
}
- using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
+ using var memoryStream = new ChunkedMemoryStream(configuration.MemoryAllocator);
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;
diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
index 9a56390d89..922088b26d 100644
--- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
+++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
@@ -100,8 +100,5 @@ internal static MemoryGroup AllocateGroup(
AllocationOptions options = AllocationOptions.None)
where T : struct
=> MemoryGroup.Allocate(memoryAllocator, totalLength, bufferAlignment, options);
-
- internal static MemoryStream AllocateFixedCapacityMemoryStream(this MemoryAllocator allocator, long length) =>
- new FixedCapacityPooledMemoryStream(length, allocator);
}
}
diff --git a/src/ImageSharp/Memory/MemoryOwnerExtensions.cs b/src/ImageSharp/Memory/MemoryOwnerExtensions.cs
index 98fd40e65b..aa475a80f1 100644
--- a/src/ImageSharp/Memory/MemoryOwnerExtensions.cs
+++ b/src/ImageSharp/Memory/MemoryOwnerExtensions.cs
@@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Memory
///
internal static class MemoryOwnerExtensions
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span GetSpan(this IMemoryOwner buffer)
=> buffer.Memory.Span;
diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
index be232c78d6..f2ff49d4e9 100644
--- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
+++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
@@ -21,8 +21,12 @@ public class BufferedStreams
private MemoryStream stream4;
private MemoryStream stream5;
private MemoryStream stream6;
+ private ChunkedMemoryStream chunkedMemoryStream1;
+ private ChunkedMemoryStream chunkedMemoryStream2;
private BufferedReadStream bufferedStream1;
private BufferedReadStream bufferedStream2;
+ private BufferedReadStream bufferedStream3;
+ private BufferedReadStream bufferedStream4;
private BufferedReadStreamWrapper bufferedStreamWrap1;
private BufferedReadStreamWrapper bufferedStreamWrap2;
@@ -35,8 +39,20 @@ public void CreateStreams()
this.stream4 = new MemoryStream(this.buffer);
this.stream5 = new MemoryStream(this.buffer);
this.stream6 = new MemoryStream(this.buffer);
+ this.stream6 = new MemoryStream(this.buffer);
+
+ this.chunkedMemoryStream1 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator);
+ this.chunkedMemoryStream1.Write(this.buffer);
+ this.chunkedMemoryStream1.Position = 0;
+
+ this.chunkedMemoryStream2 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator);
+ this.chunkedMemoryStream2.Write(this.buffer);
+ this.chunkedMemoryStream2.Position = 0;
+
this.bufferedStream1 = new BufferedReadStream(Configuration.Default, this.stream3);
this.bufferedStream2 = new BufferedReadStream(Configuration.Default, this.stream4);
+ this.bufferedStream3 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream1);
+ this.bufferedStream4 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream2);
this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5);
this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6);
}
@@ -46,8 +62,12 @@ public void DestroyStreams()
{
this.bufferedStream1?.Dispose();
this.bufferedStream2?.Dispose();
+ this.bufferedStream3?.Dispose();
+ this.bufferedStream4?.Dispose();
this.bufferedStreamWrap1?.Dispose();
this.bufferedStreamWrap2?.Dispose();
+ this.chunkedMemoryStream1?.Dispose();
+ this.chunkedMemoryStream2?.Dispose();
this.stream1?.Dispose();
this.stream2?.Dispose();
this.stream3?.Dispose();
@@ -86,6 +106,21 @@ public int BufferedReadStreamRead()
return r;
}
+ [Benchmark]
+ public int BufferedReadStreamChunkedRead()
+ {
+ int r = 0;
+ BufferedReadStream reader = this.bufferedStream3;
+ byte[] b = this.chunk2;
+
+ for (int i = 0; i < reader.Length / 2; i++)
+ {
+ r += reader.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
[Benchmark]
public int BufferedReadStreamWrapRead()
{
@@ -129,6 +164,20 @@ public int BufferedReadStreamReadByte()
return r;
}
+ [Benchmark]
+ public int BufferedReadStreamChunkedReadByte()
+ {
+ int r = 0;
+ BufferedReadStream reader = this.bufferedStream4;
+
+ for (int i = 0; i < reader.Length; i++)
+ {
+ r += reader.ReadByte();
+ }
+
+ return r;
+ }
+
[Benchmark]
public int BufferedReadStreamWrapReadByte()
{
@@ -167,40 +216,46 @@ private static byte[] CreateTestBytes()
}
/*
- BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19041
+ BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.450 (2004/?/20H1)
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
- .NET Core SDK=3.1.301
- [Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
- Job-LKLBOT : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT
- Job-RSTMKF : .NET Core 2.1.19 (CoreCLR 4.6.28928.01, CoreFX 4.6.28928.04), X64 RyuJIT
- Job-PZIHIV : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
+ .NET Core SDK=3.1.401
+ [Host] : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
+ Job-OKZLUV : .NET Framework 4.8 (4.8.4084.0), X64 RyuJIT
+ Job-CPYMXV : .NET Core 2.1.21 (CoreCLR 4.6.29130.01, CoreFX 4.6.29130.02), X64 RyuJIT
+ Job-BSGVGU : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
IterationCount=3 LaunchCount=1 WarmupCount=3
-| Method | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
-|------------------------------- |-------------- |----------:|------------:|-----------:|------:|--------:|------:|------:|------:|----------:|
-| StandardStreamRead | .NET 4.7.2 | 63.238 us | 49.7827 us | 2.7288 us | 0.66 | 0.13 | - | - | - | - |
-| BufferedReadStreamRead | .NET 4.7.2 | 66.092 us | 0.4273 us | 0.0234 us | 0.69 | 0.11 | - | - | - | - |
-| BufferedReadStreamWrapRead | .NET 4.7.2 | 26.216 us | 3.0527 us | 0.1673 us | 0.27 | 0.04 | - | - | - | - |
-| StandardStreamReadByte | .NET 4.7.2 | 97.900 us | 261.7204 us | 14.3458 us | 1.00 | 0.00 | - | - | - | - |
-| BufferedReadStreamReadByte | .NET 4.7.2 | 97.260 us | 1.2979 us | 0.0711 us | 1.01 | 0.15 | - | - | - | - |
-| BufferedReadStreamWrapReadByte | .NET 4.7.2 | 19.170 us | 2.2296 us | 0.1222 us | 0.20 | 0.03 | - | - | - | - |
-| ArrayReadByte | .NET 4.7.2 | 12.878 us | 11.1292 us | 0.6100 us | 0.13 | 0.02 | - | - | - | - |
-| | | | | | | | | | | |
-| StandardStreamRead | .NET Core 2.1 | 60.618 us | 131.7038 us | 7.2191 us | 0.78 | 0.10 | - | - | - | - |
-| BufferedReadStreamRead | .NET Core 2.1 | 30.006 us | 25.2499 us | 1.3840 us | 0.38 | 0.02 | - | - | - | - |
-| BufferedReadStreamWrapRead | .NET Core 2.1 | 29.241 us | 6.5020 us | 0.3564 us | 0.37 | 0.01 | - | - | - | - |
-| StandardStreamReadByte | .NET Core 2.1 | 78.074 us | 15.8463 us | 0.8686 us | 1.00 | 0.00 | - | - | - | - |
-| BufferedReadStreamReadByte | .NET Core 2.1 | 14.737 us | 20.1510 us | 1.1045 us | 0.19 | 0.01 | - | - | - | - |
-| BufferedReadStreamWrapReadByte | .NET Core 2.1 | 13.234 us | 1.4711 us | 0.0806 us | 0.17 | 0.00 | - | - | - | - |
-| ArrayReadByte | .NET Core 2.1 | 9.373 us | 0.6108 us | 0.0335 us | 0.12 | 0.00 | - | - | - | - |
-| | | | | | | | | | | |
-| StandardStreamRead | .NET Core 3.1 | 52.151 us | 19.9456 us | 1.0933 us | 0.65 | 0.03 | - | - | - | - |
-| BufferedReadStreamRead | .NET Core 3.1 | 29.217 us | 0.2490 us | 0.0136 us | 0.36 | 0.01 | - | - | - | - |
-| BufferedReadStreamWrapRead | .NET Core 3.1 | 32.962 us | 7.1382 us | 0.3913 us | 0.41 | 0.02 | - | - | - | - |
-| StandardStreamReadByte | .NET Core 3.1 | 80.310 us | 45.0350 us | 2.4685 us | 1.00 | 0.00 | - | - | - | - |
-| BufferedReadStreamReadByte | .NET Core 3.1 | 13.092 us | 0.6268 us | 0.0344 us | 0.16 | 0.00 | - | - | - | - |
-| BufferedReadStreamWrapReadByte | .NET Core 3.1 | 13.282 us | 3.8689 us | 0.2121 us | 0.17 | 0.01 | - | - | - | - |
-| ArrayReadByte | .NET Core 3.1 | 9.349 us | 2.9860 us | 0.1637 us | 0.12 | 0.00 | - | - | - | - |
+ | Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
+ |---------------------------------- |----------- |-------------- |-----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:|
+ | StandardStreamRead | Job-OKZLUV | .NET 4.7.2 | 66.785 us | 15.768 us | 0.8643 us | 0.83 | 0.01 | - | - | - | - |
+ | BufferedReadStreamRead | Job-OKZLUV | .NET 4.7.2 | 97.389 us | 17.658 us | 0.9679 us | 1.21 | 0.01 | - | - | - | - |
+ | BufferedReadStreamChunkedRead | Job-OKZLUV | .NET 4.7.2 | 96.006 us | 16.286 us | 0.8927 us | 1.20 | 0.02 | - | - | - | - |
+ | BufferedReadStreamWrapRead | Job-OKZLUV | .NET 4.7.2 | 37.064 us | 14.640 us | 0.8024 us | 0.46 | 0.02 | - | - | - | - |
+ | StandardStreamReadByte | Job-OKZLUV | .NET 4.7.2 | 80.315 us | 26.676 us | 1.4622 us | 1.00 | 0.00 | - | - | - | - |
+ | BufferedReadStreamReadByte | Job-OKZLUV | .NET 4.7.2 | 118.706 us | 38.013 us | 2.0836 us | 1.48 | 0.00 | - | - | - | - |
+ | BufferedReadStreamChunkedReadByte | Job-OKZLUV | .NET 4.7.2 | 115.437 us | 33.352 us | 1.8282 us | 1.44 | 0.01 | - | - | - | - |
+ | BufferedReadStreamWrapReadByte | Job-OKZLUV | .NET 4.7.2 | 16.449 us | 11.400 us | 0.6249 us | 0.20 | 0.00 | - | - | - | - |
+ | ArrayReadByte | Job-OKZLUV | .NET 4.7.2 | 10.416 us | 1.866 us | 0.1023 us | 0.13 | 0.00 | - | - | - | - |
+ | | | | | | | | | | | | |
+ | StandardStreamRead | Job-CPYMXV | .NET Core 2.1 | 71.425 us | 50.441 us | 2.7648 us | 0.82 | 0.03 | - | - | - | - |
+ | BufferedReadStreamRead | Job-CPYMXV | .NET Core 2.1 | 32.816 us | 6.655 us | 0.3648 us | 0.38 | 0.01 | - | - | - | - |
+ | BufferedReadStreamChunkedRead | Job-CPYMXV | .NET Core 2.1 | 31.995 us | 7.751 us | 0.4249 us | 0.37 | 0.01 | - | - | - | - |
+ | BufferedReadStreamWrapRead | Job-CPYMXV | .NET Core 2.1 | 31.970 us | 4.170 us | 0.2286 us | 0.37 | 0.01 | - | - | - | - |
+ | StandardStreamReadByte | Job-CPYMXV | .NET Core 2.1 | 86.909 us | 18.565 us | 1.0176 us | 1.00 | 0.00 | - | - | - | - |
+ | BufferedReadStreamReadByte | Job-CPYMXV | .NET Core 2.1 | 14.596 us | 10.889 us | 0.5969 us | 0.17 | 0.01 | - | - | - | - |
+ | BufferedReadStreamChunkedReadByte | Job-CPYMXV | .NET Core 2.1 | 13.629 us | 1.569 us | 0.0860 us | 0.16 | 0.00 | - | - | - | - |
+ | BufferedReadStreamWrapReadByte | Job-CPYMXV | .NET Core 2.1 | 13.566 us | 1.743 us | 0.0956 us | 0.16 | 0.00 | - | - | - | - |
+ | ArrayReadByte | Job-CPYMXV | .NET Core 2.1 | 9.771 us | 6.658 us | 0.3650 us | 0.11 | 0.00 | - | - | - | - |
+ | | | | | | | | | | | | |
+ | StandardStreamRead | Job-BSGVGU | .NET Core 3.1 | 53.265 us | 65.819 us | 3.6078 us | 0.81 | 0.05 | - | - | - | - |
+ | BufferedReadStreamRead | Job-BSGVGU | .NET Core 3.1 | 33.163 us | 9.569 us | 0.5245 us | 0.51 | 0.01 | - | - | - | - |
+ | BufferedReadStreamChunkedRead | Job-BSGVGU | .NET Core 3.1 | 33.001 us | 6.114 us | 0.3351 us | 0.50 | 0.01 | - | - | - | - |
+ | BufferedReadStreamWrapRead | Job-BSGVGU | .NET Core 3.1 | 29.448 us | 7.120 us | 0.3902 us | 0.45 | 0.01 | - | - | - | - |
+ | StandardStreamReadByte | Job-BSGVGU | .NET Core 3.1 | 65.619 us | 6.732 us | 0.3690 us | 1.00 | 0.00 | - | - | - | - |
+ | BufferedReadStreamReadByte | Job-BSGVGU | .NET Core 3.1 | 13.989 us | 3.464 us | 0.1899 us | 0.21 | 0.00 | - | - | - | - |
+ | BufferedReadStreamChunkedReadByte | Job-BSGVGU | .NET Core 3.1 | 13.806 us | 1.710 us | 0.0938 us | 0.21 | 0.00 | - | - | - | - |
+ | BufferedReadStreamWrapReadByte | Job-BSGVGU | .NET Core 3.1 | 13.690 us | 1.523 us | 0.0835 us | 0.21 | 0.00 | - | - | - | - |
+ | ArrayReadByte | Job-BSGVGU | .NET Core 3.1 | 10.792 us | 8.228 us | 0.4510 us | 0.16 | 0.01 | - | - | - | - |
*/
}
diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
new file mode 100644
index 0000000000..00a178c8fd
--- /dev/null
+++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
@@ -0,0 +1,379 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.IO
+{
+ ///
+ /// Tests for the class.
+ ///
+ public class ChunkedMemoryStreamTests
+ {
+ private readonly MemoryAllocator allocator;
+
+ public ChunkedMemoryStreamTests()
+ {
+ this.allocator = Configuration.Default.MemoryAllocator;
+ }
+
+ [Fact]
+ public void MemoryStream_Ctor_InvalidCapacities()
+ {
+ Assert.Throws(() => new ChunkedMemoryStream(int.MinValue, this.allocator));
+ Assert.Throws(() => new ChunkedMemoryStream(0, this.allocator));
+ }
+
+ [Fact]
+ public void MemoryStream_GetPositionTest_Negative()
+ {
+ using var ms = new ChunkedMemoryStream(this.allocator);
+ long iCurrentPos = ms.Position;
+ for (int i = -1; i > -6; i--)
+ {
+ Assert.Throws(() => ms.Position = i);
+ Assert.Equal(ms.Position, iCurrentPos);
+ }
+ }
+
+ [Fact]
+ public void MemoryStream_ReadTest_Negative()
+ {
+ var ms2 = new ChunkedMemoryStream(this.allocator);
+
+ Assert.Throws(() => ms2.Read(null, 0, 0));
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0));
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, -1));
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, 2, 0));
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 2));
+
+ ms2.Dispose();
+
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 1));
+ }
+
+ [Theory]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
+ public void MemoryStream_ReadByteTest(int length)
+ {
+ using MemoryStream ms = this.CreateTestStream(length);
+ using var cms = new ChunkedMemoryStream(this.allocator);
+
+ ms.CopyTo(cms);
+ cms.Position = 0;
+ var expected = ms.ToArray();
+
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], cms.ReadByte());
+ }
+ }
+
+ [Theory]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
+ public void MemoryStream_ReadByteBufferTest(int length)
+ {
+ using MemoryStream ms = this.CreateTestStream(length);
+ using var cms = new ChunkedMemoryStream(this.allocator);
+
+ ms.CopyTo(cms);
+ cms.Position = 0;
+ var expected = ms.ToArray();
+ var buffer = new byte[2];
+ for (int i = 0; i < expected.Length; i += 2)
+ {
+ cms.Read(buffer);
+ Assert.Equal(expected[i], buffer[0]);
+ Assert.Equal(expected[i + 1], buffer[1]);
+ }
+ }
+
+ [Theory]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
+ [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
+ [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
+ public void MemoryStream_ReadByteBufferSpanTest(int length)
+ {
+ using MemoryStream ms = this.CreateTestStream(length);
+ using var cms = new ChunkedMemoryStream(this.allocator);
+
+ ms.CopyTo(cms);
+ cms.Position = 0;
+ var expected = ms.ToArray();
+ Span buffer = new byte[2];
+ for (int i = 0; i < expected.Length; i += 2)
+ {
+ cms.Read(buffer);
+ Assert.Equal(expected[i], buffer[0]);
+ Assert.Equal(expected[i + 1], buffer[1]);
+ }
+ }
+
+ [Fact]
+ public void MemoryStream_WriteToTests()
+ {
+ 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 };
+
+ // [] Write to memoryStream, check the memoryStream
+ ms2.Write(bytArr, 0, bytArr.Length);
+
+ 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]);
+ }
+ }
+
+ // [] Write to memoryStream, check the memoryStream
+ using (var ms2 = new ChunkedMemoryStream(this.allocator))
+ using (var ms3 = new ChunkedMemoryStream(this.allocator))
+ {
+ byte[] bytArrRet;
+ byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+
+ ms2.Write(bytArr, 0, bytArr.Length);
+ ms2.WriteTo(ms3);
+ ms3.Position = 0;
+ bytArrRet = new byte[(int)ms3.Length];
+ ms3.Read(bytArrRet, 0, (int)ms3.Length);
+ for (int i = 0; i < bytArr.Length; i++)
+ {
+ Assert.Equal(bytArr[i], bytArrRet[i]);
+ }
+ }
+ }
+
+ [Fact]
+ public void MemoryStream_WriteToSpanTests()
+ {
+ using (var ms2 = new ChunkedMemoryStream(this.allocator))
+ {
+ Span bytArrRet;
+ Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+
+ // [] Write to memoryStream, check the memoryStream
+ ms2.Write(bytArr, 0, bytArr.Length);
+
+ 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]);
+ }
+ }
+
+ // [] Write to memoryStream, check the memoryStream
+ using (var ms2 = new ChunkedMemoryStream(this.allocator))
+ using (var ms3 = new ChunkedMemoryStream(this.allocator))
+ {
+ Span bytArrRet;
+ Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
+
+ ms2.Write(bytArr, 0, bytArr.Length);
+ ms2.WriteTo(ms3);
+ ms3.Position = 0;
+ bytArrRet = new byte[(int)ms3.Length];
+ ms3.Read(bytArrRet, 0, (int)ms3.Length);
+ for (int i = 0; i < bytArr.Length; i++)
+ {
+ Assert.Equal(bytArr[i], bytArrRet[i]);
+ }
+ }
+ }
+
+ [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 };
+
+ 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]);
+ }
+ }
+ }
+
+ [Fact]
+ public void MemoryStream_WriteToTests_Negative()
+ {
+ using var ms2 = new ChunkedMemoryStream(this.allocator);
+ Assert.Throws(() => ms2.WriteTo(null));
+
+ ms2.Write(new byte[] { 1 }, 0, 1);
+ var readonlyStream = new MemoryStream(new byte[1028], false);
+ Assert.Throws(() => ms2.WriteTo(readonlyStream));
+
+ readonlyStream.Dispose();
+
+ // [] Pass in a closed stream
+ Assert.Throws(() => ms2.WriteTo(readonlyStream));
+ }
+
+ [Fact]
+ public void MemoryStream_CopyTo_Invalid()
+ {
+ ChunkedMemoryStream memoryStream;
+ const string BufferSize = "bufferSize";
+ using (memoryStream = new ChunkedMemoryStream(this.allocator))
+ {
+ const string Destination = "destination";
+ Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null));
+
+ // Validate the destination parameter first.
+ Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0));
+ Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1));
+
+ // Then bufferSize.
+ Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense.
+ Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
+ }
+
+ // After the Stream is disposed, we should fail on all CopyTos.
+ Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated.
+ Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
+
+ ChunkedMemoryStream disposedStream = memoryStream;
+
+ // We should throw first for the source being disposed...
+ Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1));
+
+ // Then for the destination being disposed.
+ memoryStream = new ChunkedMemoryStream(this.allocator);
+ Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1));
+ memoryStream.Dispose();
+ }
+
+ [Theory]
+ [MemberData(nameof(CopyToData))]
+ public void CopyTo(Stream source, byte[] expected)
+ {
+ using var destination = new ChunkedMemoryStream(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());
+ }
+
+ public static IEnumerable GetAllTestImages()
+ {
+ IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories)
+ .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase));
+
+ var result = new List();
+ foreach (string path in allImageFiles)
+ {
+ result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length));
+ }
+
+ return result;
+ }
+
+ public static IEnumerable AllTestImages = GetAllTestImages();
+
+ [Theory]
+ [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)]
+ public void DecoderIntegrationTest(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ return;
+ }
+
+ Image expected;
+ try
+ {
+ expected = provider.GetImage();
+ }
+ catch
+ {
+ // The image is invalid
+ return;
+ }
+
+ string fullPath = Path.Combine(
+ TestEnvironment.InputImagesDirectoryFullPath,
+ ((TestImageProvider.FileProvider)provider).FilePath);
+
+ using FileStream fs = File.OpenRead(fullPath);
+ using var nonSeekableStream = new NonSeekableStream(fs);
+
+ var actual = Image.Load(nonSeekableStream);
+
+ ImageComparer.Exact.VerifySimilarity(expected, actual);
+ }
+
+ public static IEnumerable