Browse Source

BufferedReadStream WIP - MagickImage not working.

pull/1269/head
James Jackson-South 6 years ago
parent
commit
545cf7d822
  1. 27
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  2. 6
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
  3. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  4. 131
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  5. 301
      src/ImageSharp/IO/BufferedReadStream.cs
  6. 293
      src/ImageSharp/IO/BufferedReadStream2.cs
  7. 23
      src/ImageSharp/Image.FromStream.cs
  8. 106
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
  9. 211
      tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs

27
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.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">The count.</param>
/// <param name="count">A byte offset relative to the origin parameter.</param>
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<byte>.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<byte>.Shared.Return(buffer);
}
public static void Read(this Stream stream, IManagedByteBuffer buffer)

6
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
/// </summary>
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;

7
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
/// <param name="successiveHigh">The successive approximation bit high end.</param>
/// <param name="successiveLow">The successive approximation bit low end.</param>
public HuffmanScanDecoder(
DoubleBufferedStreamReader stream,
Stream stream,
JpegFrame frame,
HuffmanTable[] dcHuffmanTables,
HuffmanTable[] acHuffmanTables,

131
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
/// </summary>
public int BitsPerPixel => this.ComponentCount * this.Frame.Precision;
/// <summary>
/// Gets the input stream.
/// </summary>
public DoubleBufferedStreamReader InputStream { get; private set; }
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
@ -170,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="marker">The buffer to read file markers to</param>
/// <param name="stream">The input stream</param>
/// <returns>The <see cref="JpegFileMarker"/></returns>
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);
}
}
/// <inheritdoc/>
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
/// <summary>
/// Processes the application header containing the JFIF identifier plus extra data.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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);
}
}
/// <summary>
/// Processes the App1 marker retrieving any stored metadata
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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
/// <summary>
/// Processes the App2 marker retrieving any stored ICC profile information
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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);
}
}
/// <summary>
/// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the tables do not match the header
/// </exception>
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
/// <summary>
/// Processes the Start of Frame marker. Specified in section B.2.2.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <param name="frameMarker">The current frame marker.</param>
/// <param name="metadataOnly">Whether to parse metadata only</param>
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.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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);
}
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
private void ProcessStartOfScanMarker()
/// <param name="stream">The input stream.</param>
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
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes
/// </summary>
/// <param name="stream">The input stream.</param>
/// <returns>The <see cref="ushort"/></returns>
[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);
}

301
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
{
/// <summary>
/// A readonly stream that add a secondary level buffer in addition to native stream
/// buffered reading to reduce the overhead of small incremental reads.
/// </summary>
internal sealed unsafe class BufferedReadStream : Stream
{
/// <summary>
/// The length, in bytes, of the underlying buffer.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="BufferedReadStream"/> class.
/// </summary>
/// <param name="stream">The input stream.</param>
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<byte>.Shared.Rent(BufferLength);
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin();
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
// This triggers a full read on first attempt.
this.readBufferIndex = BufferLength;
}
/// <summary>
/// Gets the length, in bytes, of the stream.
/// </summary>
public override long Length => this.streamLength;
/// <summary>
/// Gets or sets the current position within the stream.
/// </summary>
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;
}
}
}
/// <inheritdoc/>
public override bool CanRead { get; } = true;
/// <inheritdoc/>
public override bool CanSeek { get; } = true;
/// <inheritdoc/>
public override bool CanWrite { get; } = false;
/// <inheritdoc/>
[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++];
}
/// <inheritdoc/>
[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);
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
/// <exception cref="NotSupportedException">This operation is not supported in <see cref="BufferedReadStream"/>.</exception>
public override void SetLength(long value)
=> throw new NotSupportedException();
/// <inheritdoc/>
/// <exception cref="NotSupportedException">This operation is not supported in <see cref="BufferedReadStream"/>.</exception>
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.readBufferHandle.Dispose();
ArrayPool<byte>.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);
}
}
}
}

293
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
{
/// <summary>
/// A readonly stream that add a secondary level buffer in addition to native stream
/// buffered reading to reduce the overhead of small incremental reads.
/// </summary>
internal sealed unsafe class BufferedReadStream2 : IDisposable
{
/// <summary>
/// The length, in bytes, of the underlying buffer.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="BufferedReadStream2"/> class.
/// </summary>
/// <param name="stream">The input stream.</param>
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<byte>.Shared.Rent(BufferLength);
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin();
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
// This triggers a full read on first attempt.
this.readBufferIndex = BufferLength;
}
/// <summary>
/// Gets the length, in bytes, of the stream.
/// </summary>
public long Length => this.length;
/// <summary>
/// Gets or sets the current position within the stream.
/// </summary>
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<byte>.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);
}
}
}
}

23
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
/// <exception cref="NotSupportedException">Thrown if the stream is not readable.</exception>
/// <returns>The format type or null if none found.</returns>
public static IImageFormat DetectFormat(Configuration configuration, Stream stream)
=> WithSeekableStream(configuration, stream, s => InternalDetectFormat(s, configuration));
=> WithSeekableStream(configuration, stream, false, s => InternalDetectFormat(s, configuration));
/// <summary>
/// Reads the raw image information from the specified stream without fully decoding it.
@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp
/// </returns>
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
/// <exception cref="UnknownImageFormatException">Image cannot be loaded.</exception>
/// <returns>A new <see cref="Image"/>.</returns>>
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));
/// <summary>
/// Decode a new instance of the <see cref="Image"/> class from the given stream.
@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>>
public static Image<TPixel> Load<TPixel>(Stream stream, IImageDecoder decoder)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(Configuration.Default, stream, s => decoder.Decode<TPixel>(Configuration.Default, s));
=> WithSeekableStream(Configuration.Default, stream, true, s => decoder.Decode<TPixel>(Configuration.Default, s));
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the given stream.
@ -177,7 +178,7 @@ namespace SixLabors.ImageSharp
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>>
public static Image<TPixel> Load<TPixel>(Configuration configuration, Stream stream, IImageDecoder decoder)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(configuration, stream, s => decoder.Decode<TPixel>(configuration, s));
=> WithSeekableStream(configuration, stream, true, s => decoder.Decode<TPixel>(configuration, s));
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the given stream.
@ -206,7 +207,7 @@ namespace SixLabors.ImageSharp
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
(Image<TPixel> img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode<TPixel>(s, configuration));
(Image<TPixel> img, IImageFormat format) data = WithSeekableStream(configuration, stream, true, s => Decode<TPixel>(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<T>(Configuration configuration, Stream stream, Func<Stream, T> action)
private static T WithSeekableStream<T>(Configuration configuration, Stream stream, bool buffer, Func<Stream, T> 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);
}

106
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;

211
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);
}
}
}
Loading…
Cancel
Save