diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs
new file mode 100644
index 000000000..798ebbdd5
--- /dev/null
+++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs
@@ -0,0 +1,544 @@
+// 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 = 4096;
+
+ // 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)
+ {
+ throw new ArgumentOutOfRangeException(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;
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+ }
+
+ ///
+ 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()
+ {
+ }
+
+ ///
+ 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));
+ if (buffer.Length - offset < count)
+ {
+ throw new ArgumentException($"{offset} subtracted from the buffer length is less than {count}");
+ }
+
+ this.EnsureNotDisposed();
+
+ if (this.readChunk is null)
+ {
+ if (this.memoryChunk is null)
+ {
+ return 0;
+ }
+
+ this.readChunk = this.memoryChunk;
+ this.readOffset = 0;
+ }
+
+ byte[] chunkBuffer = this.writeChunk.Buffer.Array;
+ int chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+
+ int bytesRead = 0;
+
+ 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.writeChunk.Buffer.Array;
+ chunkSize = this.readChunk.Length;
+ if (this.readChunk.Next is null)
+ {
+ chunkSize = this.writeOffset;
+ }
+ }
+
+ int readCount = Math.Min(count, chunkSize - this.readOffset);
+ Buffer.BlockCopy(chunkBuffer, this.readOffset, buffer, offset, readCount);
+ offset += readCount;
+ count -= readCount;
+ this.readOffset += readCount;
+ bytesRead += readCount;
+ }
+
+ return bytesRead;
+ }
+
+ ///
+ 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.writeChunk.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.writeChunk.Buffer.Array;
+ }
+
+ return chunkBuffer[this.readOffset++];
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ 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;
+
+ 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.Array;
+ chunkSize = this.writeChunk.Length;
+ }
+
+ int copyCount = Math.Min(count, chunkSize - this.writeOffset);
+ Buffer.BlockCopy(buffer, offset, chunkBuffer, this.writeOffset, copyCount);
+ 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.AggressiveInlining)]
+ private MemoryChunk AllocateMemoryChunk()
+ {
+ IManagedByteBuffer buffer = this.allocator.AllocateManagedByteBuffer(this.chunkLength);
+ return new MemoryChunk
+ {
+ Buffer = buffer,
+ Next = null,
+ Length = this.chunkLength
+ };
+ }
+
+ 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/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs
index ee148cd25..b57fa9a6c 100644
--- a/src/ImageSharp/Image.FromStream.cs
+++ b/src/ImageSharp/Image.FromStream.cs
@@ -8,6 +8,7 @@ using System.Text;
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 @@ namespace SixLabors.ImageSharp
}
// 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 @@ namespace SixLabors.ImageSharp
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/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
new file mode 100644
index 000000000..65ad93d5b
--- /dev/null
+++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
@@ -0,0 +1,183 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+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 ChunkedPooledMemoryStream_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(null, () => ms2.Read(new byte[] { 1 }, 2, 0));
+ Assert.Throws(null, () => ms2.Read(new byte[] { 1 }, 0, 2));
+
+ ms2.Dispose();
+
+ Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 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 FileStream, check the filestream
+ 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_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