diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
index 5d8668257..e811543e3 100644
--- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs
+++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
@@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.IO;
using SixLabors.ImageSharp.Memory;
-#if !SUPPORTS_SPAN_STREAM
-using System.Buffers;
-#endif
namespace SixLabors.ImageSharp
{
@@ -40,7 +38,7 @@ namespace SixLabors.ImageSharp
/// Skips the number of bytes in the given stream.
///
/// The stream.
- /// The count.
+ /// A byte offset relative to the origin parameter.
public static void Skip(this Stream stream, int count)
{
if (count < 1)
@@ -51,21 +49,22 @@ namespace SixLabors.ImageSharp
if (stream.CanSeek)
{
stream.Seek(count, SeekOrigin.Current); // Position += count;
+ return;
}
- else
+
+ var buffer = ArrayPool.Shared.Rent(count);
+ while (count > 0)
{
- var foo = new byte[count];
- while (count > 0)
+ int bytesRead = stream.Read(buffer, 0, count);
+ if (bytesRead == 0)
{
- int bytesRead = stream.Read(foo, 0, count);
- if (bytesRead == 0)
- {
- break;
- }
-
- count -= bytesRead;
+ break;
}
+
+ count -= bytesRead;
}
+
+ ArrayPool.Shared.Return(buffer);
}
public static void Read(this Stream stream, IManagedByteBuffer buffer)
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
index 34fe1aecb..8a027f2b6 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
@@ -1,8 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using System.IO;
using System.Runtime.CompilerServices;
-using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal struct HuffmanScanBuffer
{
- private readonly DoubleBufferedStreamReader stream;
+ private readonly Stream stream;
// The entropy encoded code buffer.
private ulong data;
@@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// Whether there is no more good data to pull from the stream for the current mcu.
private bool badData;
- public HuffmanScanBuffer(DoubleBufferedStreamReader stream)
+ public HuffmanScanBuffer(Stream stream)
{
this.stream = stream;
this.data = 0ul;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index fbb2b5272..742b2ab88 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -2,10 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.IO;
-using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
@@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private readonly JpegFrame frame;
private readonly HuffmanTable[] dcHuffmanTables;
private readonly HuffmanTable[] acHuffmanTables;
- private readonly DoubleBufferedStreamReader stream;
+ private readonly Stream stream;
private readonly JpegComponent[] components;
// The restart interval.
@@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// The successive approximation bit high end.
/// The successive approximation bit low end.
public HuffmanScanDecoder(
- DoubleBufferedStreamReader stream,
+ Stream stream,
JpegFrame frame,
HuffmanTable[] dcHuffmanTables,
HuffmanTable[] acHuffmanTables,
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 951fec1d4..102e80b0a 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -9,7 +9,6 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
-using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -129,11 +128,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
public int BitsPerPixel => this.ComponentCount * this.Frame.Precision;
- ///
- /// Gets the input stream.
- ///
- public DoubleBufferedStreamReader InputStream { get; private set; }
-
///
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
///
@@ -170,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// The buffer to read file markers to
/// The input stream
/// The
- public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStreamReader stream)
+ public static JpegFileMarker FindNextFileMarker(byte[] marker, Stream stream)
{
int value = stream.Read(marker, 0, 2);
@@ -239,19 +233,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public void ParseStream(Stream stream, bool metadataOnly = false)
{
this.Metadata = new ImageMetadata();
- this.InputStream = new DoubleBufferedStreamReader(this.configuration.MemoryAllocator, stream);
// Check for the Start Of Image marker.
- this.InputStream.Read(this.markerBuffer, 0, 2);
+ stream.Read(this.markerBuffer, 0, 2);
var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI)
{
JpegThrowHelper.ThrowImageFormatException("Missing SOI marker.");
}
- this.InputStream.Read(this.markerBuffer, 0, 2);
+ stream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1];
- fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2);
+ fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
this.QuantizationTables = new Block8x8F[4];
// Only assign what we need
@@ -270,20 +263,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (!fileMarker.Invalid)
{
// Get the marker length
- int remaining = this.ReadUint16() - 2;
+ int remaining = this.ReadUint16(stream) - 2;
switch (fileMarker.Marker)
{
case JpegConstants.Markers.SOF0:
case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2:
- this.ProcessStartOfFrameMarker(remaining, fileMarker, metadataOnly);
+ this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly);
break;
case JpegConstants.Markers.SOS:
if (!metadataOnly)
{
- this.ProcessStartOfScanMarker();
+ this.ProcessStartOfScanMarker(stream);
break;
}
else
@@ -297,41 +290,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (metadataOnly)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
}
else
{
- this.ProcessDefineHuffmanTablesMarker(remaining);
+ this.ProcessDefineHuffmanTablesMarker(stream, remaining);
}
break;
case JpegConstants.Markers.DQT:
- this.ProcessDefineQuantizationTablesMarker(remaining);
+ this.ProcessDefineQuantizationTablesMarker(stream, remaining);
break;
case JpegConstants.Markers.DRI:
if (metadataOnly)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
}
else
{
- this.ProcessDefineRestartIntervalMarker(remaining);
+ this.ProcessDefineRestartIntervalMarker(stream, remaining);
}
break;
case JpegConstants.Markers.APP0:
- this.ProcessApplicationHeaderMarker(remaining);
+ this.ProcessApplicationHeaderMarker(stream, remaining);
break;
case JpegConstants.Markers.APP1:
- this.ProcessApp1Marker(remaining);
+ this.ProcessApp1Marker(stream, remaining);
break;
case JpegConstants.Markers.APP2:
- this.ProcessApp2Marker(remaining);
+ this.ProcessApp2Marker(stream, remaining);
break;
case JpegConstants.Markers.APP3:
@@ -345,33 +338,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.APP11:
case JpegConstants.Markers.APP12:
case JpegConstants.Markers.APP13:
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
break;
case JpegConstants.Markers.APP14:
- this.ProcessApp14Marker(remaining);
+ this.ProcessApp14Marker(stream, remaining);
break;
case JpegConstants.Markers.APP15:
case JpegConstants.Markers.COM:
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
break;
}
}
// Read on.
- fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream);
+ fileMarker = FindNextFileMarker(this.markerBuffer, stream);
}
}
///
public void Dispose()
{
- this.InputStream?.Dispose();
this.Frame?.Dispose();
// Set large fields to null.
- this.InputStream = null;
this.Frame = null;
this.dcHuffmanTables = null;
this.acHuffmanTables = null;
@@ -485,18 +476,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Processes the application header containing the JFIF identifier plus extra data.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessApplicationHeaderMarker(int remaining)
+ private void ProcessApplicationHeaderMarker(Stream stream, int remaining)
{
// We can only decode JFif identifiers.
if (remaining < JFifMarker.Length)
{
// Skip the application header length
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
return;
}
- this.InputStream.Read(this.temp, 0, JFifMarker.Length);
+ stream.Read(this.temp, 0, JFifMarker.Length);
remaining -= JFifMarker.Length;
JFifMarker.TryParse(this.temp, out this.jFif);
@@ -504,26 +496,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// TODO: thumbnail
if (remaining > 0)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
}
}
///
/// Processes the App1 marker retrieving any stored metadata
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessApp1Marker(int remaining)
+ private void ProcessApp1Marker(Stream stream, int remaining)
{
const int Exif00 = 6;
if (remaining < Exif00 || this.IgnoreMetadata)
{
// Skip the application header length
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
return;
}
var profile = new byte[remaining];
- this.InputStream.Read(profile, 0, remaining);
+ stream.Read(profile, 0, remaining);
if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker))
{
@@ -544,26 +537,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Processes the App2 marker retrieving any stored ICC profile information
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessApp2Marker(int remaining)
+ private void ProcessApp2Marker(Stream stream, int remaining)
{
// Length is 14 though we only need to check 12.
const int Icclength = 14;
if (remaining < Icclength || this.IgnoreMetadata)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
return;
}
var identifier = new byte[Icclength];
- this.InputStream.Read(identifier, 0, Icclength);
+ stream.Read(identifier, 0, Icclength);
remaining -= Icclength; // We have read it by this point
if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker))
{
this.isIcc = true;
var profile = new byte[remaining];
- this.InputStream.Read(profile, 0, remaining);
+ stream.Read(profile, 0, remaining);
if (this.iccData is null)
{
@@ -578,7 +572,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
else
{
// Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this.
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
}
}
@@ -586,42 +580,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessApp14Marker(int remaining)
+ private void ProcessApp14Marker(Stream stream, int remaining)
{
const int MarkerLength = AdobeMarker.Length;
if (remaining < MarkerLength)
{
// Skip the application header length
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
return;
}
- this.InputStream.Read(this.temp, 0, MarkerLength);
+ stream.Read(this.temp, 0, MarkerLength);
remaining -= MarkerLength;
AdobeMarker.TryParse(this.temp, out this.adobe);
if (remaining > 0)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
}
}
///
/// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
///
/// Thrown if the tables do not match the header
///
- private void ProcessDefineQuantizationTablesMarker(int remaining)
+ private void ProcessDefineQuantizationTablesMarker(Stream stream, int remaining)
{
while (remaining > 0)
{
bool done = false;
remaining--;
- int quantizationTableSpec = this.InputStream.ReadByte();
+ int quantizationTableSpec = stream.ReadByte();
int tableIndex = quantizationTableSpec & 15;
// Max index. 4 Tables max.
@@ -641,7 +637,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break;
}
- this.InputStream.Read(this.temp, 0, 64);
+ stream.Read(this.temp, 0, 64);
remaining -= 64;
ref Block8x8F table = ref this.QuantizationTables[tableIndex];
@@ -661,7 +657,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break;
}
- this.InputStream.Read(this.temp, 0, 128);
+ stream.Read(this.temp, 0, 128);
remaining -= 128;
ref Block8x8F table = ref this.QuantizationTables[tableIndex];
@@ -697,10 +693,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Processes the Start of Frame marker. Specified in section B.2.2.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
/// The current frame marker.
/// Whether to parse metadata only
- private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
+ private void ProcessStartOfFrameMarker(Stream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
{
if (this.Frame != null)
{
@@ -709,7 +706,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Read initial marker definitions.
const int length = 6;
- this.InputStream.Read(this.temp, 0, length);
+ stream.Read(this.temp, 0, length);
// We only support 8-bit and 12-bit precision.
if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1)
@@ -747,7 +744,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowBadMarker("SOFn", remaining);
}
- this.InputStream.Read(this.temp, 0, remaining);
+ stream.Read(this.temp, 0, remaining);
// No need to pool this. They max out at 4
this.Frame.ComponentIds = new byte[this.ComponentCount];
@@ -794,8 +791,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Processes a Define Huffman Table marker, and initializes a huffman
/// struct from its contents. Specified in section B.2.4.2.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessDefineHuffmanTablesMarker(int remaining)
+ private void ProcessDefineHuffmanTablesMarker(Stream stream, int remaining)
{
int length = remaining;
@@ -804,7 +802,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanData.GetSpan());
for (int i = 2; i < remaining;)
{
- byte huffmanTableSpec = (byte)this.InputStream.ReadByte();
+ byte huffmanTableSpec = (byte)stream.ReadByte();
int tableType = huffmanTableSpec >> 4;
int tableIndex = huffmanTableSpec & 15;
@@ -820,7 +818,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowImageFormatException("Bad Huffman Table index.");
}
- this.InputStream.Read(huffmanData.Array, 0, 16);
+ stream.Read(huffmanData.Array, 0, 16);
using (IManagedByteBuffer codeLengths = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(17, AllocationOptions.Clean))
{
@@ -841,7 +839,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
using (IManagedByteBuffer huffmanValues = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean))
{
- this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum);
+ stream.Read(huffmanValues.Array, 0, codeLengthSum);
i += 17 + codeLengthSum;
@@ -860,32 +858,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in
/// macroblocks
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessDefineRestartIntervalMarker(int remaining)
+ private void ProcessDefineRestartIntervalMarker(Stream stream, int remaining)
{
if (remaining != 2)
{
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining);
}
- this.resetInterval = this.ReadUint16();
+ this.resetInterval = this.ReadUint16(stream);
}
///
/// Processes the SOS (Start of scan marker).
///
- private void ProcessStartOfScanMarker()
+ /// The input stream.
+ private void ProcessStartOfScanMarker(Stream stream)
{
if (this.Frame is null)
{
JpegThrowHelper.ThrowImageFormatException("No readable SOFn (Start Of Frame) marker found.");
}
- int selectorsCount = this.InputStream.ReadByte();
+ int selectorsCount = stream.ReadByte();
for (int i = 0; i < selectorsCount; i++)
{
int componentIndex = -1;
- int selector = this.InputStream.ReadByte();
+ int selector = stream.ReadByte();
for (int j = 0; j < this.Frame.ComponentIds.Length; j++)
{
@@ -903,20 +903,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
ref JpegComponent component = ref this.Frame.Components[componentIndex];
- int tableSpec = this.InputStream.ReadByte();
+ int tableSpec = stream.ReadByte();
component.DCHuffmanTableId = tableSpec >> 4;
component.ACHuffmanTableId = tableSpec & 15;
this.Frame.ComponentOrder[i] = (byte)componentIndex;
}
- this.InputStream.Read(this.temp, 0, 3);
+ stream.Read(this.temp, 0, 3);
int spectralStart = this.temp[0];
int spectralEnd = this.temp[1];
int successiveApproximation = this.temp[2];
var sd = new HuffmanScanDecoder(
- this.InputStream,
+ stream,
this.Frame,
this.dcHuffmanTables,
this.acHuffmanTables,
@@ -944,11 +944,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Reads a from the stream advancing it by two bytes
///
+ /// The input stream.
/// The
[MethodImpl(InliningOptions.ShortMethod)]
- private ushort ReadUint16()
+ private ushort ReadUint16(Stream stream)
{
- this.InputStream.Read(this.markerBuffer, 0, 2);
+ stream.Read(this.markerBuffer, 0, 2);
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}
diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs
new file mode 100644
index 000000000..776fb123f
--- /dev/null
+++ b/src/ImageSharp/IO/BufferedReadStream.cs
@@ -0,0 +1,301 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.IO
+{
+ ///
+ /// A readonly stream that add a secondary level buffer in addition to native stream
+ /// buffered reading to reduce the overhead of small incremental reads.
+ ///
+ internal sealed unsafe class BufferedReadStream : Stream
+ {
+ ///
+ /// The length, in bytes, of the underlying buffer.
+ ///
+ public const int BufferLength = 8192;
+
+ private const int MaxBufferIndex = BufferLength - 1;
+
+ private readonly Stream stream;
+
+ private readonly int streamLength;
+
+ private readonly byte[] readBuffer;
+
+ private MemoryHandle readBufferHandle;
+
+ private readonly byte* pinnedReadBuffer;
+
+ // Index within our buffer, not reader position.
+ private int readBufferIndex;
+
+ // Matches what the stream position would be without buffering
+ private int readerPosition;
+
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The input stream.
+ public BufferedReadStream(Stream stream)
+ {
+ Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable.");
+ Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable.");
+
+ // Ensure all underlying buffers have been flushed before we attempt to read the stream.
+ // User streams may have opted to throw from Flush if CanWrite is false
+ // (although the abstract Stream does not do so).
+ if (stream.CanWrite)
+ {
+ stream.Flush();
+ }
+
+ this.stream = stream;
+ this.Position = (int)stream.Position;
+ this.streamLength = (int)stream.Length;
+
+ this.readBuffer = ArrayPool.Shared.Rent(BufferLength);
+ this.readBufferHandle = new Memory(this.readBuffer).Pin();
+ this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
+
+ // This triggers a full read on first attempt.
+ this.readBufferIndex = BufferLength;
+ }
+
+ ///
+ /// Gets the length, in bytes, of the stream.
+ ///
+ public override long Length => this.streamLength;
+
+ ///
+ /// Gets or sets the current position within the stream.
+ ///
+ public override long Position
+ {
+ get => this.readerPosition;
+
+ set
+ {
+ // Only reset readBufferIndex if we are out of bounds of our working buffer
+ // otherwise we should simply move the value by the diff.
+ int v = (int)value;
+ if (this.IsInReadBuffer(v, out int index))
+ {
+ this.readBufferIndex = index;
+ this.readerPosition = v;
+ }
+ else
+ {
+ this.readerPosition = v;
+ this.stream.Seek(value, SeekOrigin.Begin);
+ this.readBufferIndex = BufferLength;
+ }
+ }
+ }
+
+ ///
+ public override bool CanRead { get; } = true;
+
+ ///
+ public override bool CanSeek { get; } = true;
+
+ ///
+ public override bool CanWrite { get; } = false;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int ReadByte()
+ {
+ if (this.readerPosition >= this.streamLength)
+ {
+ return -1;
+ }
+
+ // Our buffer has been read.
+ // We need to refill and start again.
+ if (this.readBufferIndex > MaxBufferIndex)
+ {
+ this.FillReadBuffer();
+ }
+
+ this.readerPosition++;
+ return this.pinnedReadBuffer[this.readBufferIndex++];
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ // Too big for our buffer. Read directly from the stream.
+ if (count > BufferLength)
+ {
+ return this.ReadToBufferDirectSlow(buffer, offset, count);
+ }
+
+ // Too big for remaining buffer but less than entire buffer length
+ // Copy to buffer then read from there.
+ if (count + this.readBufferIndex > BufferLength)
+ {
+ return this.ReadToBufferViaCopySlow(buffer, offset, count);
+ }
+
+ return this.ReadToBufferViaCopyFast(buffer, offset, count);
+ }
+
+ ///
+ public override void Flush()
+ {
+ // Reset the stream position to match reader position.
+ if (this.readerPosition != this.stream.Position)
+ {
+ this.stream.Seek(this.readerPosition, SeekOrigin.Begin);
+ this.readerPosition = (int)this.stream.Position;
+ }
+
+ // Reset to trigger full read on next attempt.
+ this.readBufferIndex = BufferLength;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ if (origin == SeekOrigin.Begin)
+ {
+ this.Position = offset;
+ }
+ else
+ {
+ this.Position += offset;
+ }
+
+ return this.readerPosition;
+ }
+
+ ///
+ /// This operation is not supported in .
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ ///
+ /// This operation is not supported in .
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!this.isDisposed)
+ {
+ this.isDisposed = true;
+ this.readBufferHandle.Dispose();
+ ArrayPool.Shared.Return(this.readBuffer);
+ this.Flush();
+
+ base.Dispose(true);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetPositionDifference(int p) => p - this.readerPosition;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsInReadBuffer(int p, out int index)
+ {
+ index = this.GetPositionDifference(p) + this.readBufferIndex;
+ return index > -1 && index < BufferLength;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void FillReadBuffer()
+ {
+ if (this.readerPosition != this.stream.Position)
+ {
+ this.stream.Seek(this.readerPosition, SeekOrigin.Begin);
+ }
+
+ this.stream.Read(this.readBuffer, 0, BufferLength);
+ this.readBufferIndex = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count)
+ {
+ int n = this.GetCopyCount(count);
+ this.CopyBytes(buffer, offset, n);
+
+ this.readerPosition += n;
+ this.readBufferIndex += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count)
+ {
+ // Refill our buffer then copy.
+ this.FillReadBuffer();
+
+ return this.ReadToBufferViaCopyFast(buffer, offset, count);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count)
+ {
+ // Read to target but don't copy to our read buffer.
+ if (this.readerPosition != this.stream.Position)
+ {
+ this.stream.Seek(this.readerPosition, SeekOrigin.Begin);
+ }
+
+ int n = this.stream.Read(buffer, offset, count);
+ this.Position += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetCopyCount(int count)
+ {
+ int n = this.streamLength - this.readerPosition;
+ if (n > count)
+ {
+ n = count;
+ }
+
+ if (n < 0)
+ {
+ n = 0;
+ }
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void CopyBytes(byte[] buffer, int offset, int count)
+ {
+ // Same as MemoryStream.
+ if (count < 9)
+ {
+ int byteCount = count;
+ int read = this.readBufferIndex;
+ byte* pinned = this.pinnedReadBuffer;
+
+ while (--byteCount > -1)
+ {
+ buffer[offset + byteCount] = pinned[read + byteCount];
+ }
+ }
+ else
+ {
+ Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/IO/BufferedReadStream2.cs b/src/ImageSharp/IO/BufferedReadStream2.cs
new file mode 100644
index 000000000..a35804ce2
--- /dev/null
+++ b/src/ImageSharp/IO/BufferedReadStream2.cs
@@ -0,0 +1,293 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.IO
+{
+ ///
+ /// A readonly stream that add a secondary level buffer in addition to native stream
+ /// buffered reading to reduce the overhead of small incremental reads.
+ ///
+ internal sealed unsafe class BufferedReadStream2 : IDisposable
+ {
+ ///
+ /// The length, in bytes, of the underlying buffer.
+ ///
+ public const int BufferLength = 8192;
+
+ private const int MaxBufferIndex = BufferLength - 1;
+
+ private readonly Stream stream;
+
+ private readonly byte[] readBuffer;
+
+ private MemoryHandle readBufferHandle;
+
+ private readonly byte* pinnedReadBuffer;
+
+ private int readBufferIndex;
+
+ private readonly int length;
+
+ private int position;
+
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The input stream.
+ public BufferedReadStream2(Stream stream)
+ {
+ Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable.");
+ Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable.");
+
+ // Ensure all underlying buffers have been flushed before we attempt to read the stream.
+ // User streams may have opted to throw from Flush if CanWrite is false
+ // (although the abstract Stream does not do so).
+ if (stream.CanWrite)
+ {
+ stream.Flush();
+ }
+
+ this.stream = stream;
+ this.Position = (int)stream.Position;
+ this.length = (int)stream.Length;
+
+ this.readBuffer = ArrayPool.Shared.Rent(BufferLength);
+ this.readBufferHandle = new Memory(this.readBuffer).Pin();
+ this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
+
+ // This triggers a full read on first attempt.
+ this.readBufferIndex = BufferLength;
+ }
+
+ ///
+ /// Gets the length, in bytes, of the stream.
+ ///
+ public long Length => this.length;
+
+ ///
+ /// Gets or sets the current position within the stream.
+ ///
+ public long Position
+ {
+ get => this.position;
+
+ set
+ {
+ // Only reset readIndex if we are out of bounds of our working buffer
+ // otherwise we should simply move the value by the diff.
+ int v = (int)value;
+ if (this.IsInReadBuffer(v, out int index))
+ {
+ this.readBufferIndex = index;
+ this.position = v;
+ }
+ else
+ {
+ this.position = v;
+ this.stream.Seek(value, SeekOrigin.Begin);
+ this.readBufferIndex = BufferLength;
+ }
+ }
+ }
+
+ public bool CanRead { get; } = true;
+
+ public bool CanSeek { get; } = true;
+
+ public bool CanWrite { get; } = false;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ReadByte()
+ {
+ if (this.position >= this.length)
+ {
+ return -1;
+ }
+
+ if (this.readBufferIndex > MaxBufferIndex)
+ {
+ this.FillReadBuffer();
+ }
+
+ this.position++;
+ return this.pinnedReadBuffer[this.readBufferIndex++];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Read(byte[] buffer, int offset, int count)
+ {
+ if (count > BufferLength)
+ {
+ return this.ReadToBufferDirectSlow(buffer, offset, count);
+ }
+
+ if (count + this.readBufferIndex > BufferLength)
+ {
+ return this.ReadToBufferViaCopySlow(buffer, offset, count);
+ }
+
+ // return this.ReadToBufferViaCopyFast(buffer, offset, count);
+ int n = this.GetCopyCount(count);
+ this.CopyBytes(buffer, offset, n);
+
+ this.position += n;
+ this.readBufferIndex += n;
+
+ return n;
+ }
+
+ public void Flush()
+ {
+ // Reset the stream position.
+ if (this.position != this.stream.Position)
+ {
+ this.stream.Seek(this.position, SeekOrigin.Begin);
+ this.position = (int)this.stream.Position;
+ }
+
+ this.readBufferIndex = BufferLength;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public long Seek(long offset, SeekOrigin origin)
+ {
+ if (origin == SeekOrigin.Begin)
+ {
+ this.Position = offset;
+ }
+ else
+ {
+ this.Position += offset;
+ }
+
+ return this.position;
+ }
+
+ public void SetLength(long value)
+ => throw new NotSupportedException();
+
+ public void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.isDisposed = true;
+ this.readBufferHandle.Dispose();
+ ArrayPool.Shared.Return(this.readBuffer);
+ this.Flush();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetPositionDifference(int p) => p - this.position;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsInReadBuffer(int p, out int index)
+ {
+ index = this.GetPositionDifference(p) + this.readBufferIndex;
+ return index > -1 && index < BufferLength;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void FillReadBuffer()
+ {
+ if (this.position != this.stream.Position)
+ {
+ this.stream.Seek(this.position, SeekOrigin.Begin);
+ }
+
+ this.stream.Read(this.readBuffer, 0, BufferLength);
+ this.readBufferIndex = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count)
+ {
+ int n = this.GetCopyCount(count);
+ this.CopyBytes(buffer, offset, n);
+
+ this.position += n;
+ this.readBufferIndex += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count)
+ {
+ // Refill our buffer then copy.
+ this.FillReadBuffer();
+
+ // return this.ReadToBufferViaCopyFast(buffer, offset, count);
+ int n = this.GetCopyCount(count);
+ this.CopyBytes(buffer, offset, n);
+
+ this.position += n;
+ this.readBufferIndex += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count)
+ {
+ // Read to target but don't copy to our read buffer.
+ if (this.position != this.stream.Position)
+ {
+ this.stream.Seek(this.position, SeekOrigin.Begin);
+ }
+
+ int n = this.stream.Read(buffer, offset, count);
+ this.Position += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetCopyCount(int count)
+ {
+ int n = this.length - this.position;
+ if (n > count)
+ {
+ n = count;
+ }
+
+ if (n < 0)
+ {
+ n = 0;
+ }
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void CopyBytes(byte[] buffer, int offset, int count)
+ {
+ // Same as MemoryStream.
+ if (count < 9)
+ {
+ int byteCount = count;
+ int read = this.readBufferIndex;
+ byte* pinned = this.pinnedReadBuffer;
+
+ while (--byteCount > -1)
+ {
+ buffer[offset + byteCount] = pinned[read + byteCount];
+ }
+ }
+ else
+ {
+ Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs
index 52d71409b..7ef9038c5 100644
--- a/src/ImageSharp/Image.FromStream.cs
+++ b/src/ImageSharp/Image.FromStream.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp
@@ -31,7 +32,7 @@ namespace SixLabors.ImageSharp
/// Thrown if the stream is not readable.
/// The format type or null if none found.
public static IImageFormat DetectFormat(Configuration configuration, Stream stream)
- => WithSeekableStream(configuration, stream, s => InternalDetectFormat(s, configuration));
+ => WithSeekableStream(configuration, stream, false, s => InternalDetectFormat(s, configuration));
///
/// Reads the raw image information from the specified stream without fully decoding it.
@@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp
///
public static IImageInfo Identify(Configuration configuration, Stream stream, out IImageFormat format)
{
- (IImageInfo info, IImageFormat format) data = WithSeekableStream(configuration, stream, s => InternalIdentity(s, configuration ?? Configuration.Default));
+ (IImageInfo info, IImageFormat format) data = WithSeekableStream(configuration, stream, false, s => InternalIdentity(s, configuration ?? Configuration.Default));
format = data.format;
return data.info;
@@ -115,7 +116,7 @@ namespace SixLabors.ImageSharp
/// Image cannot be loaded.
/// A new .>
public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) =>
- WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s));
+ WithSeekableStream(configuration, stream, true, s => decoder.Decode(configuration, s));
///
/// Decode a new instance of the class from the given stream.
@@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp
/// A new .>
public static Image Load(Stream stream, IImageDecoder decoder)
where TPixel : unmanaged, IPixel
- => WithSeekableStream(Configuration.Default, stream, s => decoder.Decode(Configuration.Default, s));
+ => WithSeekableStream(Configuration.Default, stream, true, s => decoder.Decode(Configuration.Default, s));
///
/// Create a new instance of the class from the given stream.
@@ -177,7 +178,7 @@ namespace SixLabors.ImageSharp
/// A new .>
public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder)
where TPixel : unmanaged, IPixel
- => WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s));
+ => WithSeekableStream(configuration, stream, true, s => decoder.Decode(configuration, s));
///
/// Create a new instance of the class from the given stream.
@@ -206,7 +207,7 @@ namespace SixLabors.ImageSharp
where TPixel : unmanaged, IPixel
{
Guard.NotNull(configuration, nameof(configuration));
- (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration));
+ (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, true, s => Decode(s, configuration));
format = data.format;
@@ -239,7 +240,7 @@ namespace SixLabors.ImageSharp
public static Image Load(Configuration configuration, Stream stream, out IImageFormat format)
{
Guard.NotNull(configuration, nameof(configuration));
- (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration));
+ (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, true, s => Decode(s, configuration));
format = data.format;
@@ -259,7 +260,7 @@ namespace SixLabors.ImageSharp
throw new UnknownImageFormatException(sb.ToString());
}
- private static T WithSeekableStream(Configuration configuration, Stream stream, Func action)
+ private static T WithSeekableStream(Configuration configuration, Stream stream, bool buffer, Func action)
{
if (!stream.CanRead)
{
@@ -273,6 +274,12 @@ namespace SixLabors.ImageSharp
stream.Position = 0;
}
+ if (buffer)
+ {
+ using var bufferedStream = new BufferedReadStream(stream);
+ return action(bufferedStream);
+ }
+
return action(stream);
}
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
index 6f3ea0e14..389a74326 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
@@ -19,8 +19,16 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
private MemoryStream stream2;
private MemoryStream stream3;
private MemoryStream stream4;
+ private MemoryStream stream5;
+ private MemoryStream stream6;
+ private MemoryStream stream7;
+ private MemoryStream stream8;
private DoubleBufferedStreamReader reader1;
private DoubleBufferedStreamReader reader2;
+ private BufferedReadStream bufferedStream1;
+ private BufferedReadStream bufferedStream2;
+ private BufferedReadStream2 bufferedStream3;
+ private BufferedReadStream2 bufferedStream4;
[GlobalSetup]
public void CreateStreams()
@@ -29,8 +37,16 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
this.stream2 = new MemoryStream(this.buffer);
this.stream3 = new MemoryStream(this.buffer);
this.stream4 = new MemoryStream(this.buffer);
+ this.stream5 = new MemoryStream(this.buffer);
+ this.stream6 = new MemoryStream(this.buffer);
+ this.stream7 = new MemoryStream(this.buffer);
+ this.stream8 = new MemoryStream(this.buffer);
this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2);
this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2);
+ this.bufferedStream1 = new BufferedReadStream(this.stream5);
+ this.bufferedStream2 = new BufferedReadStream(this.stream6);
+ this.bufferedStream3 = new BufferedReadStream2(this.stream7);
+ this.bufferedStream4 = new BufferedReadStream2(this.stream8);
}
[GlobalCleanup]
@@ -40,8 +56,74 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
this.stream2?.Dispose();
this.stream3?.Dispose();
this.stream4?.Dispose();
+ this.stream5?.Dispose();
+ this.stream6?.Dispose();
this.reader1?.Dispose();
this.reader2?.Dispose();
+ this.bufferedStream1?.Dispose();
+ this.bufferedStream2?.Dispose();
+ this.bufferedStream3?.Dispose();
+ this.bufferedStream4?.Dispose();
+ }
+
+ [Benchmark]
+ public int StandardStreamRead()
+ {
+ int r = 0;
+ Stream stream = this.stream1;
+ byte[] b = this.chunk1;
+
+ for (int i = 0; i < stream.Length / 2; i++)
+ {
+ r += stream.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int DoubleBufferedStreamRead()
+ {
+ int r = 0;
+ DoubleBufferedStreamReader reader = this.reader2;
+ byte[] b = this.chunk2;
+
+ for (int i = 0; i < reader.Length / 2; i++)
+ {
+ r += reader.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int BufferedStreamRead()
+ {
+ int r = 0;
+ BufferedReadStream reader = this.bufferedStream2;
+ byte[] b = this.chunk2;
+
+ for (int i = 0; i < reader.Length / 2; i++)
+ {
+ r += reader.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int BufferedStreamWrapRead()
+ {
+ int r = 0;
+ BufferedReadStream2 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(Baseline = true)]
@@ -59,25 +141,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
}
[Benchmark]
- public int StandardStreamRead()
+ public int DoubleBufferedStreamReadByte()
{
int r = 0;
- Stream stream = this.stream1;
- byte[] b = this.chunk1;
+ DoubleBufferedStreamReader reader = this.reader1;
- for (int i = 0; i < stream.Length / 2; i++)
+ for (int i = 0; i < reader.Length; i++)
{
- r += stream.Read(b, 0, 2);
+ r += reader.ReadByte();
}
return r;
}
[Benchmark]
- public int DoubleBufferedStreamReadByte()
+ public int BufferedStreamReadByte()
{
int r = 0;
- DoubleBufferedStreamReader reader = this.reader1;
+ BufferedReadStream reader = this.bufferedStream2;
for (int i = 0; i < reader.Length; i++)
{
@@ -88,22 +169,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
}
[Benchmark]
- public int DoubleBufferedStreamRead()
+ public int BufferedStreamWrapReadByte()
{
int r = 0;
- DoubleBufferedStreamReader reader = this.reader2;
- byte[] b = this.chunk2;
+ BufferedReadStream2 reader = this.bufferedStream4;
- for (int i = 0; i < reader.Length / 2; i++)
+ for (int i = 0; i < reader.Length; i++)
{
- r += reader.Read(b, 0, 2);
+ r += reader.ReadByte();
}
return r;
}
[Benchmark]
- public int SimpleReadByte()
+ public int ArrayReadByte()
{
byte[] b = this.buffer;
int r = 0;
diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
new file mode 100644
index 000000000..992e2536d
--- /dev/null
+++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
@@ -0,0 +1,211 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using SixLabors.ImageSharp.IO;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.IO
+{
+ public class BufferedReadStreamTests
+ {
+ [Fact]
+ public void BufferedStreamCanReadSingleByteFromOrigin()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ byte[] expected = stream.ToArray();
+ using (var reader = new BufferedReadStream(stream))
+ {
+ Assert.Equal(expected[0], reader.ReadByte());
+
+ // We've read a whole chunk but increment by 1 in our reader.
+ Assert.Equal(BufferedReadStream.BufferLength, stream.Position);
+ Assert.Equal(1, reader.Position);
+ }
+
+ // Position of the stream should be reset on disposal.
+ Assert.Equal(1, stream.Position);
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamCanReadSingleByteFromOffset()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ byte[] expected = stream.ToArray();
+ const int offset = 5;
+ using (var reader = new BufferedReadStream(stream))
+ {
+ reader.Position = offset;
+
+ Assert.Equal(expected[offset], reader.ReadByte());
+
+ // We've read a whole chunk but increment by 1 in our reader.
+ Assert.Equal(BufferedReadStream.BufferLength + offset, stream.Position);
+ Assert.Equal(offset + 1, reader.Position);
+ }
+
+ Assert.Equal(offset + 1, stream.Position);
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamCanReadSubsequentSingleByteCorrectly()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ byte[] expected = stream.ToArray();
+ int i;
+ using (var reader = new BufferedReadStream(stream))
+ {
+ for (i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], reader.ReadByte());
+ Assert.Equal(i + 1, reader.Position);
+
+ if (i < BufferedReadStream.BufferLength)
+ {
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength);
+ }
+ else if (i >= BufferedReadStream.BufferLength && i < BufferedReadStream.BufferLength * 2)
+ {
+ // We should have advanced to the second chunk now.
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2);
+ }
+ else
+ {
+ // We should have advanced to the third chunk now.
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3);
+ }
+ }
+ }
+
+ Assert.Equal(i, stream.Position);
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamCanReadMultipleBytesFromOrigin()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ var buffer = new byte[2];
+ byte[] expected = stream.ToArray();
+ using (var reader = new BufferedReadStream(stream))
+ {
+ Assert.Equal(2, reader.Read(buffer, 0, 2));
+ Assert.Equal(expected[0], buffer[0]);
+ Assert.Equal(expected[1], buffer[1]);
+
+ // We've read a whole chunk but increment by the buffer length in our reader.
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength);
+ Assert.Equal(buffer.Length, reader.Position);
+ }
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamCanReadSubsequentMultipleByteCorrectly()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ var buffer = new byte[2];
+ byte[] expected = stream.ToArray();
+ using (var reader = new BufferedReadStream(stream))
+ {
+ for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2)
+ {
+ Assert.Equal(2, reader.Read(buffer, 0, 2));
+ Assert.Equal(expected[o], buffer[0]);
+ Assert.Equal(expected[o + 1], buffer[1]);
+ Assert.Equal(o + 2, reader.Position);
+
+ int offset = i * 2;
+ if (offset < BufferedReadStream.BufferLength)
+ {
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength);
+ }
+ else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2)
+ {
+ // We should have advanced to the second chunk now.
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2);
+ }
+ else
+ {
+ // We should have advanced to the third chunk now.
+ Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3);
+ }
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamCanSkip()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ byte[] expected = stream.ToArray();
+ using (var reader = new BufferedReadStream(stream))
+ {
+ int skip = 50;
+ int plusOne = 1;
+ int skip2 = BufferedReadStream.BufferLength;
+
+ // Skip
+ reader.Skip(skip);
+ Assert.Equal(skip, reader.Position);
+ Assert.Equal(stream.Position, reader.Position);
+
+ // Read
+ Assert.Equal(expected[skip], reader.ReadByte());
+
+ // Skip Again
+ reader.Skip(skip2);
+
+ // First Skip + First Read + Second Skip
+ int position = skip + plusOne + skip2;
+
+ Assert.Equal(position, reader.Position);
+ Assert.Equal(stream.Position, reader.Position);
+ Assert.Equal(expected[position], reader.ReadByte());
+ }
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamReadsSmallStream()
+ {
+ // Create a stream smaller than the default buffer length
+ using (MemoryStream stream = this.CreateTestStream(BufferedReadStream.BufferLength / 4))
+ {
+ byte[] expected = stream.ToArray();
+ const int offset = 5;
+ using (var reader = new BufferedReadStream(stream))
+ {
+ reader.Position = offset;
+
+ Assert.Equal(expected[offset], reader.ReadByte());
+
+ // We've read a whole length of the stream but increment by 1 in our reader.
+ Assert.Equal(BufferedReadStream.BufferLength / 4, stream.Position);
+ Assert.Equal(offset + 1, reader.Position);
+ }
+
+ Assert.Equal(offset + 1, stream.Position);
+ }
+ }
+
+ private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3)
+ {
+ var buffer = new byte[length];
+ var random = new Random();
+ random.NextBytes(buffer);
+
+ return new MemoryStream(buffer);
+ }
+ }
+}