Browse Source

Rewrite ChunkedMemoryStream

pull/2828/head
James Jackson-South 1 year ago
parent
commit
48645f8b47
  1. 4
      src/ImageSharp/Formats/ImageEncoder.cs
  2. 628
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  3. 38
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  4. 93
      tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
  5. 6
      tests/ImageSharp.Tests/Image/NonSeekableStream.cs

4
src/ImageSharp/Formats/ImageEncoder.cs

@ -49,7 +49,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);
}
@ -65,7 +65,7 @@ public abstract class ImageEncoder : IImageEncoder
}
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await DoEncodeAsync(ms);
ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)

628
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Collections;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
@ -12,44 +13,24 @@ namespace SixLabors.ImageSharp.IO;
/// Chunks are allocated by the <see cref="MemoryAllocator"/> assigned via the constructor
/// and is designed to take advantage of buffer pooling when available.
/// </summary>
internal sealed class ChunkedMemoryStream : Stream
/// <summary>Provides an in-memory stream composed of non-contiguous chunks.</summary>
public class ChunkedMemoryStream : Stream
{
// The memory allocator.
private readonly MemoryAllocator allocator;
private readonly MemoryChunkBuffer memoryChunkBuffer;
private readonly byte[] singleReadBuffer = new byte[1];
// 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 long length;
private long position;
private int currentChunk;
private int currentChunkIndex;
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;
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(MemoryAllocator allocator)
{
this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator;
}
=> this.memoryChunkBuffer = new(allocator);
/// <inheritdoc/>
public override bool CanRead => !this.isDisposed;
@ -66,25 +47,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 +57,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);
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Flush()
{
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
@ -188,41 +93,23 @@ internal sealed class ChunkedMemoryStream : Stream
=> throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
public override int ReadByte()
{
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
this.EnsureNotDisposed();
if (this.position >= this.length)
{
base.Dispose(disposing);
return -1;
}
}
/// <inheritdoc/>
public override void Flush()
{
_ = this.Read(this.singleReadBuffer, 0, 1);
return this.singleReadBuffer[^1];
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count)
{
this.EnsureNotDisposed();
Guard.NotNull(buffer, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset));
Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count));
@ -230,111 +117,63 @@ 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));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(Span<byte> buffer) => this.ReadImpl(buffer);
private int ReadImpl(Span<byte> buffer)
public override int Read(Span<byte> buffer)
{
this.EnsureNotDisposed();
if (this.readChunk is null)
int offset = 0;
int count = buffer.Length;
int bytesRead = 0;
long bytesToRead = this.length - this.position;
if (bytesToRead > count)
{
if (this.memoryChunk is null)
{
return 0;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
bytesToRead = count;
}
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
if (bytesToRead <= 0)
{
chunkSize = this.writeOffset;
// Already at the end of the stream, nothing to read
return 0;
}
int bytesRead = 0;
int offset = 0;
int count = buffer.Length;
while (count > 0)
while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length)
{
if (this.readOffset == chunkSize)
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk];
int n = (int)Math.Min(bytesToRead, int.MaxValue);
int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex;
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;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int ReadByte()
{
this.EnsureNotDisposed();
// Read n bytes from the current chunk
chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, 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.currentChunkIndex = 0;
this.currentChunk++;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
}
IMemoryOwner<byte> 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.currentChunkIndex += n;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.readChunk.Buffer;
}
return chunkBuffer.GetSpan()[this.readOffset++];
this.position += bytesRead;
return bytesRead;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Write(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
@ -344,157 +183,200 @@ 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));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Write(ReadOnlySpan<byte> buffer) => this.WriteImpl(buffer);
private void WriteImpl(ReadOnlySpan<byte> buffer)
public override void Write(ReadOnlySpan<byte> buffer)
{
this.EnsureNotDisposed();
if (this.memoryChunk is null)
int offset = 0;
int count = buffer.Length;
int bytesWritten = 0;
long bytesToWrite = this.memoryChunkBuffer.Length - this.position;
// Ensure we have enough capacity to write the data.
while (bytesToWrite < count)
{
this.memoryChunk = this.AllocateMemoryChunk();
this.writeChunk = this.memoryChunk;
this.writeOffset = 0;
this.memoryChunkBuffer.Expand();
bytesToWrite = this.memoryChunkBuffer.Length - this.position;
}
Guard.NotNull(this.writeChunk);
if (bytesToWrite > count)
{
bytesToWrite = count;
}
Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan();
int chunkSize = this.writeChunk.Length;
int count = buffer.Length;
int offset = 0;
while (count > 0)
while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length)
{
if (this.writeOffset == chunkSize)
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk];
int n = (int)Math.Min(bytesToWrite, int.MaxValue);
int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex;
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.currentChunkIndex, n));
bytesToWrite -= n;
offset += n;
bytesWritten += n;
offset += copyCount;
count -= copyCount;
this.writeOffset += copyCount;
if (moveToNextChunk)
{
this.currentChunkIndex = 0;
this.currentChunk++;
}
else
{
this.currentChunkIndex += n;
}
}
this.position += bytesWritten;
this.length += bytesWritten;
}
/// <inheritdoc/>
public override void WriteByte(byte value)
/// <summary>
/// Writes the entire contents of this memory stream to another stream.
/// </summary>
/// <param name="stream">The stream to write this memory stream to.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is <see langword="null"/>.</exception>
/// <exception cref="ObjectDisposedException">The current or target stream is closed.</exception>
public void WriteTo(Stream stream)
{
Guard.NotNull(stream, nameof(stream));
this.EnsureNotDisposed();
if (this.memoryChunk is null)
this.Position = 0;
int bytesRead = 0;
long bytesToRead = this.length - this.position;
if (bytesToRead <= 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);
while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length)
{
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk];
int n = (int)Math.Min(bytesToRead, int.MaxValue);
int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex;
if (n >= remainingBytesInCurrentChunk)
{
n = remainingBytesInCurrentChunk;
moveToNextChunk = true;
}
IMemoryOwner<byte> chunkBuffer = this.writeChunk.Buffer;
int chunkSize = this.writeChunk.Length;
// Read n bytes from the current chunk
stream.Write(chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, 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.currentChunkIndex = 0;
this.currentChunk++;
}
else
{
this.currentChunkIndex += n;
}
}
chunkBuffer.GetSpan()[this.writeOffset++] = value;
this.position += bytesRead;
}
/// <summary>
/// Copy entire buffer into an array.
/// Writes the stream contents to a byte array, regardless of the <see cref="Position"/> property.
/// </summary>
/// <returns>The <see cref="T:byte[]"/>.</returns>
/// <returns>A new <see cref="T:byte[]"/>.</returns>
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;
}
/// <summary>
/// Write remainder of this stream to another stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public void WriteTo(Stream stream)
/// <inheritdoc/>
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.currentChunk = 0;
this.currentChunkIndex = 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<byte> 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.currentChunk = this.memoryChunkBuffer.ChunkCount - 1;
this.currentChunkIndex = this.memoryChunkBuffer[this.currentChunk].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.currentChunk = currentChunkIndex;
// Safe to cast here as we know the offset is less than the chunk length.
this.currentChunkIndex = (int)offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -507,48 +389,82 @@ 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 : IEnumerable<MemoryChunk>, IDisposable
{
private readonly List<MemoryChunk> 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<byte> buffer = this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++)));
public int ChunkCount => this.memoryChunks.Count;
public long Length { get; private set; }
return new MemoryChunk(buffer)
public MemoryChunk this[int index] => this.memoryChunks[index];
public void Expand()
{
Next = null,
Length = buffer.Length()
};
}
IMemoryOwner<byte> buffer =
this.allocator.Allocate<byte>(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;
this.Dispose(true);
GC.SuppressFinalize(this);
}
}
[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
public IEnumerator<MemoryChunk> GetEnumerator()
=> ((IEnumerable<MemoryChunk>)this.memoryChunks).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)this.memoryChunks).GetEnumerator();
[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 void Dispose(bool disposing)
{
if (!this.isDisposed)
{
if (disposing)
{
foreach (MemoryChunk chunk in this.memoryChunks)
{
chunk.Dispose();
}
this.memoryChunks.Clear();
}
this.Length = 0;
this.isDisposed = true;
}
}
}
private sealed class MemoryChunk : IDisposable
@ -559,8 +475,6 @@ internal sealed class ChunkedMemoryStream : Stream
public IMemoryOwner<byte> Buffer { get; }
public MemoryChunk? Next { get; set; }
public int Length { get; init; }
private void Dispose(bool disposing)

38
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -544,6 +544,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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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%

93
tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

@ -30,7 +30,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 +42,7 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_ReadTest_Negative()
{
var ms2 = new ChunkedMemoryStream(this.allocator);
ChunkedMemoryStream ms2 = new(this.allocator);
Assert.Throws<ArgumentNullException>(() => ms2.Read(null, 0, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => ms2.Read(new byte[] { 1 }, -1, 0));
@ -63,8 +63,8 @@ public class ChunkedMemoryStreamTests
[InlineData(DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
using var cms = new ChunkedMemoryStream(this.allocator);
using MemoryStream ms = CreateTestStream(length);
using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@ -84,8 +84,8 @@ public class ChunkedMemoryStreamTests
[InlineData(DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteBufferTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
using var cms = new ChunkedMemoryStream(this.allocator);
using MemoryStream ms = CreateTestStream(length);
using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@ -107,8 +107,8 @@ public class ChunkedMemoryStreamTests
[InlineData(DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteBufferSpanTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
using var cms = new ChunkedMemoryStream(this.allocator);
using MemoryStream ms = CreateTestStream(length);
using ChunkedMemoryStream cms = new(this.allocator);
ms.CopyTo(cms);
cms.Position = 0;
@ -125,7 +125,7 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_WriteToTests()
{
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 };
@ -133,7 +133,7 @@ public class ChunkedMemoryStreamTests
// [] 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,8 +146,8 @@ 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 };
@ -167,7 +167,7 @@ public class ChunkedMemoryStreamTests
[Fact]
public void MemoryStream_WriteToSpanTests()
{
using (var ms2 = new ChunkedMemoryStream(this.allocator))
using (ChunkedMemoryStream ms2 = new(this.allocator))
{
Span<byte> bytArrRet;
Span<byte> bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
@ -175,10 +175,12 @@ public class ChunkedMemoryStreamTests
// [] 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 +190,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<byte> bytArrRet;
Span<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];
@ -209,37 +212,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<ArgumentNullException>(() => 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<NotSupportedException>(() => ms2.WriteTo(readonlyStream));
readonlyStream.Dispose();
@ -286,7 +287,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,10 +298,10 @@ public class ChunkedMemoryStreamTests
IEnumerable<string> allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories)
.Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase));
var result = new List<string>();
List<string> result = new();
foreach (string path in allImageFiles)
{
result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length));
result.Add(path[TestEnvironment.InputImagesDirectoryFullPath.Length..]);
}
return result;
@ -334,9 +335,9 @@ public class ChunkedMemoryStreamTests
((TestImageProvider<TPixel>.FileProvider)provider).FilePath);
using FileStream fs = File.OpenRead(fullPath);
using var nonSeekableStream = new NonSeekableStream(fs);
using NonSeekableStream nonSeekableStream = new(fs);
var actual = Image.Load<TPixel>(nonSeekableStream);
Image<TPixel> actual = Image.Load<TPixel>(nonSeekableStream);
ImageComparer.Exact.VerifySimilarity(expected, actual);
}
@ -345,27 +346,27 @@ public class ChunkedMemoryStreamTests
{
// Stream is positioned @ beginning of data
byte[] data1 = new byte[] { 1, 2, 3 };
var stream1 = new MemoryStream(data1);
MemoryStream stream1 = new(data1);
yield return new object[] { stream1, data1 };
// Stream is positioned in the middle of data
byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 };
var stream2 = new MemoryStream(data2) { Position = 1 };
MemoryStream stream2 = new(data2) { Position = 1 };
yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } };
// Stream is positioned after end of data
byte[] data3 = data2;
var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 };
MemoryStream stream3 = new(data3) { Position = data3.Length + 1 };
yield return new object[] { stream3, Array.Empty<byte>() };
}
private MemoryStream CreateTestStream(int length)
private static MemoryStream CreateTestStream(int length)
{
byte[] buffer = new byte[length];
var random = new Random();
Random random = new();
random.NextBytes(buffer);
return new MemoryStream(buffer);

6
tests/ImageSharp.Tests/Image/NonSeekableStream.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Tests;
@ -14,7 +14,7 @@ internal class NonSeekableStream : Stream
public override bool CanSeek => false;
public override bool CanWrite => false;
public override bool CanWrite => this.dataStream.CanWrite;
public override bool CanTimeout => this.dataStream.CanTimeout;
@ -91,5 +91,5 @@ internal class NonSeekableStream : Stream
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotImplementedException();
=> this.dataStream.Write(buffer, offset, count);
}

Loading…
Cancel
Save