diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
index 637751f6e4..f2367d488a 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)
@@ -50,14 +48,16 @@ namespace SixLabors.ImageSharp
if (stream.CanSeek)
{
- stream.Seek(count, SeekOrigin.Current); // Position += count;
+ stream.Seek(count, SeekOrigin.Current);
+ return;
}
- else
+
+ byte[] buffer = ArrayPool.Shared.Rent(count);
+ try
{
- var foo = new byte[count];
while (count > 0)
{
- int bytesRead = stream.Read(foo, 0, count);
+ int bytesRead = stream.Read(buffer, 0, count);
if (bytesRead == 0)
{
break;
@@ -66,6 +66,10 @@ namespace SixLabors.ImageSharp
count -= bytesRead;
}
}
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
}
public static void Read(this Stream stream, IManagedByteBuffer buffer)
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
index 16da086c9e..7e8ac07215 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -29,8 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black;
///
- public async Task> DecodeAsync(Configuration configuration, Stream stream)
- where TPixel : unmanaged, IPixel
+ public Image Decode(Configuration configuration, Stream stream)
+ where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -38,19 +39,24 @@ namespace SixLabors.ImageSharp.Formats.Bmp
try
{
- return await decoder.DecodeAsync(stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Decode(bufferedStream);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- throw new InvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex);
+ throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex);
}
}
+ ///
+ public Image Decode(Configuration configuration, Stream stream)
+ => this.Decode(configuration, stream);
+
///
- public Image Decode(Configuration configuration, Stream stream)
- where TPixel : unmanaged, IPixel
+ public async Task> DecodeAsync(Configuration configuration, Stream stream)
+ where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -58,28 +64,28 @@ namespace SixLabors.ImageSharp.Formats.Bmp
try
{
- return decoder.Decode(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- throw new InvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex);
+ throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex);
}
}
///
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
-
- ///
- public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
+ public async Task DecodeAsync(Configuration configuration, Stream stream)
+ => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
///
public IImageInfo Identify(Configuration configuration, Stream stream)
{
Guard.NotNull(stream, nameof(stream));
- return new BmpDecoderCore(configuration, this).Identify(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return new BmpDecoderCore(configuration, this).Identify(bufferedStream);
}
///
@@ -87,7 +93,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
Guard.NotNull(stream, nameof(stream));
- return new BmpDecoderCore(configuration, this).IdentifyAsync(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return new BmpDecoderCore(configuration, this).IdentifyAsync(bufferedStream);
}
}
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 4b14061cf8..ea8fd11a86 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -4,10 +4,8 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
-using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@@ -62,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// The stream to decode from.
///
- private Stream stream;
+ private BufferedReadStream stream;
///
/// The metadata.
@@ -120,7 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height);
///
- public Image Decode(Stream stream)
+ public Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
try
@@ -199,7 +197,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- public IImageInfo Identify(Stream stream)
+ public IImageInfo Identify(BufferedReadStream stream)
{
this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata);
@@ -1355,7 +1353,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
/// the bytes per color palette entry's can be 3 bytes instead of 4.
- private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palette)
+ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette)
{
this.stream = stream;
diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs
index 5f4fdd0fa6..2a5fde6acd 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs
@@ -1,9 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System;
using System.IO;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@@ -26,54 +26,66 @@ namespace SixLabors.ImageSharp.Formats.Gif
public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All;
///
- public async Task> DecodeAsync(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
var decoder = new GifDecoderCore(configuration, this);
try
{
- return await decoder.DecodeAsync(stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Decode(bufferedStream);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- GifThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
+ ///
+ public Image Decode(Configuration configuration, Stream stream)
+ => this.Decode(configuration, stream);
+
///
- public Image Decode(Configuration configuration, Stream stream)
+ public async Task> DecodeAsync(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
var decoder = new GifDecoderCore(configuration, this);
try
{
- return decoder.Decode(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- GifThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
+ ///
+ public async Task DecodeAsync(Configuration configuration, Stream stream)
+ => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
+
///
public IImageInfo Identify(Configuration configuration, Stream stream)
{
Guard.NotNull(stream, nameof(stream));
var decoder = new GifDecoderCore(configuration, this);
- return decoder.Identify(stream);
+
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Identify(bufferedStream);
}
///
@@ -82,13 +94,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(stream, nameof(stream));
var decoder = new GifDecoderCore(configuration, this);
- return decoder.IdentifyAsync(stream);
- }
- ///
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
-
- ///
- public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.IdentifyAsync(bufferedStream);
+ }
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index e4c98799ba..78ffee8bdb 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
///
/// The currently loaded stream.
///
- private Stream stream;
+ private BufferedReadStream stream;
///
/// The global color table.
@@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator;
///
- public Image Decode(Stream stream)
+ public Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
Image image = null;
@@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
///
- public IImageInfo Identify(Stream stream)
+ public IImageInfo Identify(BufferedReadStream stream)
{
try
{
@@ -572,7 +572,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Reads the logical screen descriptor and global color table blocks
///
/// The stream containing image data.
- private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream)
+ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{
this.stream = stream;
diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
index 6a975951c4..9eaa55566b 100644
--- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
@@ -3,10 +3,9 @@
using System;
using System.Buffers;
-using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Gif
@@ -29,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
///
/// The stream to decode.
///
- private readonly Stream stream;
+ private readonly BufferedReadStream stream;
///
/// The prefix buffer.
@@ -52,8 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
///
/// The to use for buffer allocations.
/// The stream to read from.
- /// is null.
- public LzwDecoder(MemoryAllocator memoryAllocator, Stream stream)
+ /// is null.
+ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs
index 3ab9123530..33748bf245 100644
--- a/src/ImageSharp/Formats/IImageDecoderInternals.cs
+++ b/src/ImageSharp/Formats/IImageDecoderInternals.cs
@@ -1,7 +1,8 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System.IO;
+using System;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats
@@ -21,18 +22,16 @@ namespace SixLabors.ImageSharp.Formats
///
/// The pixel format.
/// The stream, where the image should be decoded from. Cannot be null.
- ///
- /// is null.
- ///
+ /// is null.
/// The decoded image.
- Image Decode(Stream stream)
+ Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel;
///
/// Reads the raw image information from the specified stream.
///
- /// The containing image data.
+ /// The containing image data.
/// The .
- IImageInfo Identify(Stream stream);
+ IImageInfo Identify(BufferedReadStream stream);
}
}
diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs
index 6bb9116cda..9d1639a090 100644
--- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs
+++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs
@@ -1,9 +1,9 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System.IO;
+using System;
using System.Threading.Tasks;
-using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats
@@ -14,42 +14,21 @@ namespace SixLabors.ImageSharp.Formats
/// Reads the raw image information from the specified stream.
///
/// The decoder.
- /// The containing image data.
- public static async Task IdentifyAsync(this IImageDecoderInternals decoder, Stream stream)
- {
- if (stream.CanSeek)
- {
- return decoder.Identify(stream);
- }
-
- using MemoryStream ms = decoder.Configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
- await stream.CopyToAsync(ms).ConfigureAwait(false);
- ms.Position = 0;
- return decoder.Identify(ms);
- }
+ /// The containing image data.
+ /// is null.
+ /// A representing the asynchronous operation.
+ public static Task IdentifyAsync(this IImageDecoderInternals decoder, BufferedReadStream stream)
+ => Task.FromResult(decoder.Identify(stream));
///
/// Decodes the image from the specified stream.
///
/// The pixel format.
/// The decoder.
- /// The stream, where the image should be decoded from. Cannot be null.
- ///
- /// is null.
- ///
- /// The decoded image.
- public static async Task> DecodeAsync(this IImageDecoderInternals decoder, Stream stream)
+ /// The containing image data.
+ /// A representing the asynchronous operation.
+ public static Task> DecodeAsync(this IImageDecoderInternals decoder, BufferedReadStream stream)
where TPixel : unmanaged, IPixel
- {
- if (stream.CanSeek)
- {
- return decoder.Decode(stream);
- }
-
- using MemoryStream ms = decoder.Configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
- await stream.CopyToAsync(ms).ConfigureAwait(false);
- ms.Position = 0;
- return decoder.Decode(ms);
- }
+ => Task.FromResult(decoder.Decode(stream));
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
index 76d5a2dd9a..7747801700 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal struct HuffmanScanBuffer
{
- private readonly DoubleBufferedStreamReader stream;
+ private readonly BufferedReadStream 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(BufferedReadStream 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 10c1b9bcf4..d6c16f8260 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -5,7 +5,6 @@ using System;
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 BufferedReadStream 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,
+ BufferedReadStream stream,
JpegFrame frame,
HuffmanTable[] dcHuffmanTables,
HuffmanTable[] acHuffmanTables,
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
index 2d2d7fb56e..c5332acb5a 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -13,13 +14,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfoDetector
{
- ///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
- ///
+ ///
public bool IgnoreMetadata { get; set; }
///
- public async Task> DecodeAsync(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -27,21 +26,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
using var decoder = new JpegDecoderCore(configuration, this);
try
{
- return await decoder.DecodeAsync(stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Decode(bufferedStream);
}
catch (InvalidMemoryOperationException ex)
{
(int w, int h) = (decoder.ImageWidth, decoder.ImageHeight);
- JpegThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex);
+ JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
+ ///
+ public Image Decode(Configuration configuration, Stream stream)
+ => this.Decode(configuration, stream);
+
///
- public Image Decode(Configuration configuration, Stream stream)
+ public async Task> DecodeAsync(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -49,23 +53,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
using var decoder = new JpegDecoderCore(configuration, this);
try
{
- return decoder.Decode(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
(int w, int h) = (decoder.ImageWidth, decoder.ImageHeight);
- JpegThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex);
+ JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
- ///
- public Image Decode(Configuration configuration, Stream stream)
- => this.Decode(configuration, stream);
-
///
public async Task DecodeAsync(Configuration configuration, Stream stream)
=> await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
@@ -75,21 +76,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
Guard.NotNull(stream, nameof(stream));
- using (var decoder = new JpegDecoderCore(configuration, this))
- {
- return decoder.Identify(stream);
- }
+ using var decoder = new JpegDecoderCore(configuration, this);
+ using var bufferedStream = new BufferedReadStream(stream);
+
+ return decoder.Identify(bufferedStream);
}
///
- public async Task IdentifyAsync(Configuration configuration, Stream stream)
+ public Task IdentifyAsync(Configuration configuration, Stream stream)
{
Guard.NotNull(stream, nameof(stream));
- using (var decoder = new JpegDecoderCore(configuration, this))
- {
- return await decoder.IdentifyAsync(stream).ConfigureAwait(false);
- }
+ using var decoder = new JpegDecoderCore(configuration, this);
+ using var bufferedStream = new BufferedReadStream(stream);
+
+ return decoder.IdentifyAsync(bufferedStream);
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index b4f37cd7f2..2956d2c11b 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -3,10 +3,8 @@
using System;
using System.Buffers.Binary;
-using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Threading.Tasks;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@@ -139,11 +137,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.
///
@@ -180,7 +173,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, BufferedReadStream stream)
{
int value = stream.Read(marker, 0, 2);
@@ -212,7 +205,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- public Image Decode(Stream stream)
+ public Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
this.ParseStream(stream);
@@ -224,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- public IImageInfo Identify(Stream stream)
+ public IImageInfo Identify(BufferedReadStream stream)
{
this.ParseStream(stream, true);
this.InitExifProfile();
@@ -240,22 +233,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The input stream
/// Whether to decode metadata only.
- public void ParseStream(Stream stream, bool metadataOnly = false)
+ public void ParseStream(BufferedReadStream 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.ThrowInvalidImageContentException("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
@@ -274,20 +266,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
@@ -301,41 +293,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:
@@ -348,37 +340,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.APP10:
case JpegConstants.Markers.APP11:
case JpegConstants.Markers.APP12:
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
break;
case JpegConstants.Markers.APP13:
- this.ProcessApp13Marker(remaining);
+ this.ProcessApp13Marker(stream, 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;
@@ -504,18 +494,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(BufferedReadStream 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);
@@ -523,26 +514,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(BufferedReadStream 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))
{
@@ -563,26 +555,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(BufferedReadStream 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)
{
@@ -597,7 +590,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);
}
}
@@ -605,21 +598,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop.
/// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks.
///
+ /// The input stream.
/// The remaining bytes in the segment block.
- private void ProcessApp13Marker(int remaining)
+ private void ProcessApp13Marker(BufferedReadStream stream, int remaining)
{
if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata)
{
- this.InputStream.Skip(remaining);
+ stream.Skip(remaining);
return;
}
- this.InputStream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length);
+ stream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length);
remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker))
{
var resourceBlockData = new byte[remaining];
- this.InputStream.Read(resourceBlockData, 0, remaining);
+ stream.Read(resourceBlockData, 0, remaining);
Span blockDataSpan = resourceBlockData.AsSpan();
while (blockDataSpan.Length > 12)
@@ -694,42 +688,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(BufferedReadStream 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(BufferedReadStream 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.
@@ -749,7 +745,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];
@@ -769,7 +765,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];
@@ -805,10 +801,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(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
{
if (this.Frame != null)
{
@@ -822,7 +819,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)
@@ -860,7 +857,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];
@@ -907,8 +904,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(BufferedReadStream stream, int remaining)
{
int length = remaining;
@@ -917,7 +915,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;
@@ -933,7 +931,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("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))
{
@@ -954,7 +952,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;
@@ -973,32 +971,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(BufferedReadStream 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(BufferedReadStream stream)
{
if (this.Frame is null)
{
JpegThrowHelper.ThrowInvalidImageContentException("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++)
{
@@ -1016,20 +1016,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,
@@ -1057,11 +1057,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(BufferedReadStream stream)
{
- this.InputStream.Read(this.markerBuffer, 0, 2);
+ stream.Read(this.markerBuffer, 0, 2);
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs
index a6a040789a..9eb9277840 100644
--- a/src/ImageSharp/Formats/Png/PngDecoder.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoder.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -13,83 +14,73 @@ namespace SixLabors.ImageSharp.Formats.Png
///
public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDetector
{
- ///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
- ///
+ ///
public bool IgnoreMetadata { get; set; }
- ///
- /// Decodes the image from the specified stream to the .
- ///
- /// The pixel format.
- /// The configuration for the image.
- /// The containing image data.
- /// The decoded image.
- public async Task> DecodeAsync(Configuration configuration, Stream stream)
+ ///
+ public Image Decode(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
var decoder = new PngDecoderCore(configuration, this);
try
{
- return await decoder.DecodeAsync(stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Decode(bufferedStream);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- PngThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
- ///
- /// Decodes the image from the specified stream to the .
- ///
- /// The pixel format.
- /// The configuration for the image.
- /// The containing image data.
- /// The decoded image.
- public Image Decode(Configuration configuration, Stream stream)
+ ///
+ public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
+
+ ///
+ public async Task> DecodeAsync(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
var decoder = new PngDecoderCore(configuration, this);
try
{
- return decoder.Decode(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- PngThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
+ ///
+ public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
+
///
public IImageInfo Identify(Configuration configuration, Stream stream)
{
var decoder = new PngDecoderCore(configuration, this);
- return decoder.Identify(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Identify(bufferedStream);
}
///
public Task IdentifyAsync(Configuration configuration, Stream stream)
{
var decoder = new PngDecoderCore(configuration, this);
- return decoder.IdentifyAsync(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.IdentifyAsync(bufferedStream);
}
-
- ///
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
-
- ///
- public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
}
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index e2b0e50fcc..89fa4e63d0 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// The stream to decode from.
///
- private Stream currentStream;
+ private BufferedReadStream currentStream;
///
/// The png header.
@@ -132,7 +132,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public Size Dimensions => new Size(this.header.Width, this.header.Height);
///
- public Image Decode(Stream stream)
+ public Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
var metadata = new ImageMetadata();
@@ -224,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
///
- public IImageInfo Identify(Stream stream)
+ public IImageInfo Identify(BufferedReadStream stream)
{
var metadata = new ImageMetadata();
PngMetadata pngMetadata = metadata.GetPngMetadata();
@@ -499,7 +499,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The compressed pixel data stream.
/// The image to decode to.
/// The png metadata
- private void DecodePixelData(Stream compressedStream, ImageFrame image, PngMetadata pngMetadata)
+ private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel
{
while (this.currentRow < this.header.Height)
@@ -555,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The compressed pixel data stream.
/// The current image.
/// The png metadata.
- private void DecodeInterlacedPixelData(Stream compressedStream, ImageFrame image, PngMetadata pngMetadata)
+ private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel
{
int pass = 0;
@@ -1027,7 +1027,8 @@ namespace SixLabors.ImageSharp.Formats.Png
private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value)
{
using (var memoryStream = new MemoryStream(compressedData.ToArray()))
- using (var inflateStream = new ZlibInflateStream(memoryStream))
+ using (var bufferedStream = new BufferedReadStream(memoryStream))
+ using (var inflateStream = new ZlibInflateStream(bufferedStream))
{
if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
{
diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
index 07316d37b7..52ef0e85ba 100644
--- a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.IO.Compression;
+using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
@@ -27,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
///
/// The inner raw memory stream.
///
- private readonly Stream innerStream;
+ private readonly BufferedReadStream innerStream;
///
/// A value indicating whether this instance of the given entity has been disposed.
@@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
/// Initializes a new instance of the class.
///
/// The inner raw stream.
- public ZlibInflateStream(Stream innerStream)
+ public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
@@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
///
/// The inner raw stream.
/// A delegate to get more data from the inner stream.
- public ZlibInflateStream(Stream innerStream, Func getData)
+ public ZlibInflateStream(BufferedReadStream innerStream, Func getData)
{
this.innerStream = innerStream;
this.getData = getData;
@@ -272,7 +273,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.currentDataRemaining -= 4;
}
- // Initialize the deflate Stream.
+ // Initialize the deflate BufferedReadStream.
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs
index 06b9ab6050..3d9b9a3d2a 100644
--- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs
+++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector
{
///
- public async Task> DecodeAsync(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -23,21 +24,26 @@ namespace SixLabors.ImageSharp.Formats.Tga
try
{
- return await decoder.DecodeAsync(stream).ConfigureAwait(false);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return decoder.Decode(bufferedStream);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- TgaThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
}
}
+ ///
+ public Image Decode(Configuration configuration, Stream stream)
+ => this.Decode(configuration, stream);
+
///
- public Image Decode(Configuration configuration, Stream stream)
+ public async Task> DecodeAsync(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
@@ -46,13 +52,14 @@ namespace SixLabors.ImageSharp.Formats.Tga
try
{
- return decoder.Decode(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;
- TgaThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
+ TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex);
// Not reachable, as the previous statement will throw a exception.
return null;
@@ -60,17 +67,16 @@ namespace SixLabors.ImageSharp.Formats.Tga
}
///
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
-
- ///
- public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
+ public async Task DecodeAsync(Configuration configuration, Stream stream)
+ => await this.DecodeAsync(configuration, stream).ConfigureAwait(false);
///
public IImageInfo Identify(Configuration configuration, Stream stream)
{
Guard.NotNull(stream, nameof(stream));
- return new TgaDecoderCore(configuration, this).Identify(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return new TgaDecoderCore(configuration, this).Identify(bufferedStream);
}
///
@@ -78,7 +84,8 @@ namespace SixLabors.ImageSharp.Formats.Tga
{
Guard.NotNull(stream, nameof(stream));
- return new TgaDecoderCore(configuration, this).IdentifyAsync(stream);
+ using var bufferedStream = new BufferedReadStream(stream);
+ return new TgaDecoderCore(configuration, this).IdentifyAsync(bufferedStream);
}
}
}
diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
index 3f6d721f6a..7cd83fedbf 100644
--- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
@@ -3,7 +3,6 @@
using System;
using System.Buffers;
-using System.IO;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@@ -45,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
///
/// The stream to decode from.
///
- private Stream currentStream;
+ private BufferedReadStream currentStream;
///
/// The bitmap decoder options.
@@ -78,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height);
///
- public Image Decode(Stream stream)
+ public Image Decode(BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
try
@@ -641,7 +640,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
}
///
- public IImageInfo Identify(Stream stream)
+ public IImageInfo Identify(BufferedReadStream stream)
{
this.ReadFileHeader(stream);
return new ImageInfo(
@@ -868,9 +867,9 @@ namespace SixLabors.ImageSharp.Formats.Tga
///
/// Reads the tga file header from the stream.
///
- /// The containing image data.
+ /// The containing image data.
/// The image origin.
- private TgaImageOrigin ReadFileHeader(Stream stream)
+ private TgaImageOrigin ReadFileHeader(BufferedReadStream stream)
{
this.currentStream = stream;
diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs
new file mode 100644
index 0000000000..8166263744
--- /dev/null
+++ b/src/ImageSharp/IO/BufferedReadStream.cs
@@ -0,0 +1,413 @@
+// Copyright (c) Six Labors.
+// 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 class BufferedReadStream : Stream
+ {
+ ///
+ /// The length, in bytes, of the underlying buffer.
+ ///
+ public const int BufferLength = 8192;
+
+ private const int MaxBufferIndex = BufferLength - 1;
+
+ private readonly byte[] readBuffer;
+
+ private MemoryHandle readBufferHandle;
+
+ private readonly unsafe byte* pinnedReadBuffer;
+
+ // Index within our buffer, not reader position.
+ private int readBufferIndex;
+
+ // Matches what the stream position would be without buffering
+ private long 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.BaseStream = stream;
+ this.Position = (int)stream.Position;
+ this.Length = stream.Length;
+
+ this.readBuffer = ArrayPool.Shared.Rent(BufferLength);
+ this.readBufferHandle = new Memory(this.readBuffer).Pin();
+ unsafe
+ {
+ this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
+ }
+
+ // This triggers a full read on first attempt.
+ this.readBufferIndex = BufferLength;
+ }
+
+ ///
+ public override long Length { get; }
+
+ ///
+ public override long Position
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.readerPosition;
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ set
+ {
+ // Only reset readBufferIndex if we are out of bounds of our working buffer
+ // otherwise we should simply move the value by the diff.
+ if (this.IsInReadBuffer(value, out long index))
+ {
+ this.readBufferIndex = (int)index;
+ this.readerPosition = value;
+ }
+ else
+ {
+ // Base stream seek will throw for us if invalid.
+ this.BaseStream.Seek(value, SeekOrigin.Begin);
+ this.readerPosition = value;
+ this.readBufferIndex = BufferLength;
+ }
+ }
+ }
+
+ ///
+ public override bool CanRead { get; } = true;
+
+ ///
+ public override bool CanSeek { get; } = true;
+
+ ///
+ public override bool CanWrite { get; } = false;
+
+ ///
+ /// Gets the underlying stream.
+ ///
+ public Stream BaseStream
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int ReadByte()
+ {
+ if (this.readerPosition >= this.Length)
+ {
+ return -1;
+ }
+
+ // Our buffer has been read.
+ // We need to refill and start again.
+ if (this.readBufferIndex > MaxBufferIndex)
+ {
+ this.FillReadBuffer();
+ }
+
+ this.readerPosition++;
+ unsafe
+ {
+ 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);
+ }
+
+#if SUPPORTS_SPAN_STREAM
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int Read(Span buffer)
+ {
+ // Too big for our buffer. Read directly from the stream.
+ int count = buffer.Length;
+ if (count > BufferLength)
+ {
+ return this.ReadToBufferDirectSlow(buffer);
+ }
+
+ // 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);
+ }
+
+ return this.ReadToBufferViaCopyFast(buffer);
+ }
+#endif
+
+ ///
+ public override void Flush()
+ {
+ // Reset the stream position to match reader position.
+ Stream baseStream = this.BaseStream;
+ if (this.readerPosition != baseStream.Position)
+ {
+ baseStream.Seek(this.readerPosition, SeekOrigin.Begin);
+ this.readerPosition = (int)baseStream.Position;
+ }
+
+ // Reset to trigger full read on next attempt.
+ this.readBufferIndex = BufferLength;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ this.Position = offset;
+ break;
+
+ case SeekOrigin.Current:
+ this.Position += offset;
+ break;
+
+ case SeekOrigin.End:
+ this.Position = this.Length - offset;
+ break;
+ }
+
+ return this.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 bool IsInReadBuffer(long newPosition, out long index)
+ {
+ index = newPosition - this.readerPosition + this.readBufferIndex;
+ return index > -1 && index < BufferLength;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void FillReadBuffer()
+ {
+ Stream baseStream = this.BaseStream;
+ if (this.readerPosition != baseStream.Position)
+ {
+ baseStream.Seek(this.readerPosition, SeekOrigin.Begin);
+ }
+
+ // Read doesn't always guarantee the full returned length so read a byte
+ // at a time until we get either our count or hit the end of the stream.
+ int n = 0;
+ int i;
+ do
+ {
+ i = baseStream.Read(this.readBuffer, n, BufferLength - n);
+ n += i;
+ }
+ while (n < BufferLength && i > 0);
+
+ this.readBufferIndex = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int ReadToBufferViaCopyFast(Span buffer)
+ {
+ int n = this.GetCopyCount(buffer.Length);
+
+ // Just straight copy. MemoryStream does the same so should be fast enough.
+ this.readBuffer.AsSpan(this.readBufferIndex, n).CopyTo(buffer);
+
+ this.readerPosition += n;
+ this.readBufferIndex += n;
+
+ return n;
+ }
+
+ [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.AggressiveInlining)]
+ private int ReadToBufferViaCopySlow(Span buffer)
+ {
+ // Refill our buffer then copy.
+ this.FillReadBuffer();
+
+ return this.ReadToBufferViaCopyFast(buffer);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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(Span buffer)
+ {
+ // Read to target but don't copy to our read buffer.
+ Stream baseStream = this.BaseStream;
+ if (this.readerPosition != baseStream.Position)
+ {
+ baseStream.Seek(this.readerPosition, SeekOrigin.Begin);
+ }
+
+ // Read doesn't always guarantee the full returned length so read a byte
+ // at a time until we get either our count or hit the end of the stream.
+ int count = buffer.Length;
+ int n = 0;
+ int i;
+ do
+ {
+ i = baseStream.Read(buffer.Slice(n, count - n));
+ n += i;
+ }
+ while (n < count && i > 0);
+
+ this.Position += 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.
+ Stream baseStream = this.BaseStream;
+ if (this.readerPosition != baseStream.Position)
+ {
+ baseStream.Seek(this.readerPosition, SeekOrigin.Begin);
+ }
+
+ // Read doesn't always guarantee the full returned length so read a byte
+ // at a time until we get either our count or hit the end of the stream.
+ int n = 0;
+ int i;
+ do
+ {
+ i = baseStream.Read(buffer, n + offset, count - n);
+ n += i;
+ }
+ while (n < count && i > 0);
+
+ this.Position += n;
+
+ return n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetCopyCount(int count)
+ {
+ long n = this.Length - this.readerPosition;
+ if (n > count)
+ {
+ return count;
+ }
+
+ if (n < 0)
+ {
+ return 0;
+ }
+
+ return (int)n;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe 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/DoubleBufferedStreamReader.cs b/src/ImageSharp/IO/DoubleBufferedStreamReader.cs
deleted file mode 100644
index 079657c832..0000000000
--- a/src/ImageSharp/IO/DoubleBufferedStreamReader.cs
+++ /dev/null
@@ -1,255 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.Buffers;
-using System.IO;
-using System.Runtime.CompilerServices;
-
-using SixLabors.ImageSharp.Memory;
-
-namespace SixLabors.ImageSharp.IO
-{
- ///
- /// A stream reader 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 DoubleBufferedStreamReader : IDisposable
- {
- ///
- /// The length, in bytes, of the buffering chunk.
- ///
- public const int ChunkLength = 8192;
-
- private const int MaxChunkIndex = ChunkLength - 1;
-
- private readonly Stream stream;
-
- private readonly IManagedByteBuffer managedBuffer;
-
- private MemoryHandle handle;
-
- private readonly byte* pinnedChunk;
-
- private readonly byte[] bufferChunk;
-
- private readonly int length;
-
- private int chunkIndex;
-
- private int position;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The to use for buffer allocations.
- /// The input stream.
- public DoubleBufferedStreamReader(MemoryAllocator memoryAllocator, Stream stream)
- {
- this.stream = stream;
- this.Position = (int)stream.Position;
- this.length = (int)stream.Length;
- this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(ChunkLength);
- this.bufferChunk = this.managedBuffer.Array;
- this.handle = this.managedBuffer.Memory.Pin();
- this.pinnedChunk = (byte*)this.handle.Pointer;
- this.chunkIndex = ChunkLength;
- }
-
- ///
- /// 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 chunkIndex if we are out of bounds of our working chunk
- // otherwise we should simply move the value by the diff.
- int v = (int)value;
- if (this.IsInChunk(v, out int index))
- {
- this.chunkIndex = index;
- this.position = v;
- }
- else
- {
- this.position = v;
- this.stream.Seek(value, SeekOrigin.Begin);
- this.chunkIndex = ChunkLength;
- }
- }
- }
-
- ///
- /// Reads a byte from the stream and advances the position within the stream by one
- /// byte, or returns -1 if at the end of the stream.
- ///
- /// The unsigned byte cast to an , or -1 if at the end of the stream.
- [MethodImpl(InliningOptions.ShortMethod)]
- public int ReadByte()
- {
- if (this.position >= this.length)
- {
- return -1;
- }
-
- if (this.chunkIndex > MaxChunkIndex)
- {
- this.FillChunk();
- }
-
- this.position++;
- return this.pinnedChunk[this.chunkIndex++];
- }
-
- ///
- /// Skips the number of bytes in the stream
- ///
- /// The number of bytes to skip.
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Skip(int count) => this.Position += count;
-
- ///
- /// Reads a sequence of bytes from the current stream and advances the position within the stream
- /// by the number of bytes read.
- ///
- ///
- /// An array of bytes. When this method returns, the buffer contains the specified
- /// byte array with the values between offset and (offset + count - 1) replaced by
- /// the bytes read from the current source.
- ///
- ///
- /// The zero-based byte offset in buffer at which to begin storing the data read
- /// from the current stream.
- ///
- /// The maximum number of bytes to be read from the current stream.
- ///
- /// The total number of bytes read into the buffer. This can be less than the number
- /// of bytes requested if that many bytes are not currently available, or zero (0)
- /// if the end of the stream has been reached.
- ///
- [MethodImpl(InliningOptions.ShortMethod)]
- public int Read(byte[] buffer, int offset, int count)
- {
- if (count > ChunkLength)
- {
- return this.ReadToBufferSlow(buffer, offset, count);
- }
-
- if (count + this.chunkIndex > ChunkLength)
- {
- return this.ReadToChunkSlow(buffer, offset, count);
- }
-
- int n = this.GetCopyCount(count);
- this.CopyBytes(buffer, offset, n);
-
- this.position += n;
- this.chunkIndex += n;
- return n;
- }
-
- ///
- public void Dispose()
- {
- this.handle.Dispose();
- this.managedBuffer?.Dispose();
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private int GetPositionDifference(int p) => p - this.position;
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private bool IsInChunk(int p, out int index)
- {
- index = this.GetPositionDifference(p) + this.chunkIndex;
- return index > -1 && index < ChunkLength;
- }
-
- [MethodImpl(InliningOptions.ColdPath)]
- private void FillChunk()
- {
- if (this.position != this.stream.Position)
- {
- this.stream.Seek(this.position, SeekOrigin.Begin);
- }
-
- this.stream.Read(this.bufferChunk, 0, ChunkLength);
- this.chunkIndex = 0;
- }
-
- [MethodImpl(InliningOptions.ColdPath)]
- private int ReadToChunkSlow(byte[] buffer, int offset, int count)
- {
- // Refill our buffer then copy.
- this.FillChunk();
-
- int n = this.GetCopyCount(count);
- this.CopyBytes(buffer, offset, n);
-
- this.position += n;
- this.chunkIndex += n;
-
- return n;
- }
-
- [MethodImpl(InliningOptions.ColdPath)]
- private int ReadToBufferSlow(byte[] buffer, int offset, int count)
- {
- // Read to target but don't copy to our chunk.
- 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(InliningOptions.ShortMethod)]
- private int GetCopyCount(int count)
- {
- int n = this.length - this.position;
- if (n > count)
- {
- n = count;
- }
-
- if (n < 0)
- {
- n = 0;
- }
-
- return n;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private void CopyBytes(byte[] buffer, int offset, int count)
- {
- if (count < 9)
- {
- int byteCount = count;
- int read = this.chunkIndex;
- byte* pinned = this.pinnedChunk;
-
- while (--byteCount > -1)
- {
- buffer[offset + byteCount] = pinned[read + byteCount];
- }
- }
- else
- {
- Buffer.BlockCopy(this.bufferChunk, this.chunkIndex, buffer, offset, count);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs
index 5330782f22..683590fd1a 100644
--- a/src/ImageSharp/Image.Decode.cs
+++ b/src/ImageSharp/Image.Decode.cs
@@ -59,7 +59,18 @@ namespace SixLabors.ImageSharp
using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(headerSize, AllocationOptions.Clean))
{
long startPosition = stream.Position;
- stream.Read(buffer.Array, 0, headerSize);
+
+ // Read doesn't always guarantee the full returned length so read a byte
+ // at a time until we get either our count or hit the end of the stream.
+ int n = 0;
+ int i;
+ do
+ {
+ i = stream.Read(buffer.Array, n, headerSize - n);
+ n += i;
+ }
+ while (n < headerSize && i > 0);
+
stream.Position = startPosition;
// Does the given stream contain enough data to fit in the header for the format
diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs
index d3fd35d5fc..beec0b1880 100644
--- a/src/ImageSharp/Image.FromStream.cs
+++ b/src/ImageSharp/Image.FromStream.cs
@@ -7,7 +7,6 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
-using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -655,7 +654,18 @@ namespace SixLabors.ImageSharp
throw new UnknownImageFormatException(sb.ToString());
}
- private static T WithSeekableStream(Configuration configuration, Stream stream, Func action)
+ ///
+ /// Performs the given action against the stream ensuring that it is seekable.
+ ///
+ /// The type of object returned from the action.
+ /// The configuration.
+ /// The input stream.
+ /// The action to perform.
+ /// The .
+ private static T WithSeekableStream(
+ Configuration configuration,
+ Stream stream,
+ Func action)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(stream, nameof(stream));
@@ -676,15 +686,21 @@ namespace SixLabors.ImageSharp
}
// We want to be able to load images from things like HttpContext.Request.Body
- using (MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length))
- {
- stream.CopyTo(memoryStream);
- memoryStream.Position = 0;
+ using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
+ stream.CopyTo(memoryStream);
+ memoryStream.Position = 0;
- return action(memoryStream);
- }
+ return action(memoryStream);
}
+ ///
+ /// Performs the given action asynchronously against the stream ensuring that it is seekable.
+ ///
+ /// The type of object returned from the action.
+ /// The configuration.
+ /// The input stream.
+ /// The action to perform.
+ /// The .
private static async Task WithSeekableStreamAsync(
Configuration configuration,
Stream stream,
@@ -712,13 +728,11 @@ namespace SixLabors.ImageSharp
return await action(stream).ConfigureAwait(false);
}
- using (MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length))
- {
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- memoryStream.Position = 0;
+ using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
- return await action(memoryStream).ConfigureAwait(false);
- }
+ return await action(memoryStream).ConfigureAwait(false);
}
}
}
diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs
index 7800a5c6f2..605f5e0da8 100644
--- a/src/ImageSharp/Image.cs
+++ b/src/ImageSharp/Image.cs
@@ -103,15 +103,15 @@ namespace SixLabors.ImageSharp
///
/// The stream to save the image to.
/// The encoder to save the image with.
- /// Thrown if the stream or encoder is null.
+ /// Thrown if the stream or encoder is null.
/// A representing the asynchronous operation.
- public async Task SaveAsync(Stream stream, IImageEncoder encoder)
+ public Task SaveAsync(Stream stream, IImageEncoder encoder)
{
Guard.NotNull(stream, nameof(stream));
Guard.NotNull(encoder, nameof(encoder));
this.EnsureNotDisposed();
- await this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)).ConfigureAwait(false);
+ return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream));
}
///
@@ -179,9 +179,8 @@ namespace SixLabors.ImageSharp
public void Visit(Image image)
where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream);
- public async Task VisitAsync(Image image)
- where TPixel : unmanaged, IPixel
- => await this.encoder.EncodeAsync(image, this.stream).ConfigureAwait(false);
+ public Task VisitAsync(Image image)
+ where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream);
}
}
}
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
index dfedf3d89b..ef098e2632 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
@@ -6,6 +6,7 @@ using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Tests;
using SDSize = System.Drawing.Size;
@@ -30,24 +31,20 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
[Benchmark(Baseline = true, Description = "System.Drawing FULL")]
public SDSize JpegSystemDrawing()
{
- using (var memoryStream = new MemoryStream(this.jpegBytes))
- {
- using (var image = System.Drawing.Image.FromStream(memoryStream))
- {
- return image.Size;
- }
- }
+ using var memoryStream = new MemoryStream(this.jpegBytes);
+ using var image = System.Drawing.Image.FromStream(memoryStream);
+ return image.Size;
}
[Benchmark(Description = "JpegDecoderCore.ParseStream")]
public void ParseStreamPdfJs()
{
- using (var memoryStream = new MemoryStream(this.jpegBytes))
- {
- var decoder = new JpegDecoderCore(Configuration.Default, new Formats.Jpeg.JpegDecoder { IgnoreMetadata = true });
- decoder.ParseStream(memoryStream);
- decoder.Dispose();
- }
+ using var memoryStream = new MemoryStream(this.jpegBytes);
+ using var bufferedStream = new BufferedReadStream(memoryStream);
+
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true });
+ decoder.ParseStream(bufferedStream);
+ decoder.Dispose();
}
// RESULTS (2019 April 23):
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs
index 2ac02c7d53..620a4d5edf 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs
@@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
public ShortClr()
{
// Job.Default.With(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3),
- this.Add(Job.Default.With(CoreRuntime.Core21).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3));
+ this.Add(Job.Default.With(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3));
}
}
}
@@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage);
- #pragma warning disable SA1115
+#pragma warning disable SA1115
[Params(
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr,
TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr,
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
deleted file mode 100644
index 77719673f5..0000000000
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.IO;
-using BenchmarkDotNet.Attributes;
-using SixLabors.ImageSharp.IO;
-
-namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
-{
- [Config(typeof(Config.ShortClr))]
- public class DoubleBufferedStreams
- {
- private readonly byte[] buffer = CreateTestBytes();
- private readonly byte[] chunk1 = new byte[2];
- private readonly byte[] chunk2 = new byte[2];
-
- private MemoryStream stream1;
- private MemoryStream stream2;
- private MemoryStream stream3;
- private MemoryStream stream4;
- private DoubleBufferedStreamReader reader1;
- private DoubleBufferedStreamReader reader2;
-
- [GlobalSetup]
- public void CreateStreams()
- {
- this.stream1 = new MemoryStream(this.buffer);
- this.stream2 = new MemoryStream(this.buffer);
- this.stream3 = new MemoryStream(this.buffer);
- this.stream4 = new MemoryStream(this.buffer);
- this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2);
- this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2);
- }
-
- [GlobalCleanup]
- public void DestroyStreams()
- {
- this.stream1?.Dispose();
- this.stream2?.Dispose();
- this.stream3?.Dispose();
- this.stream4?.Dispose();
- this.reader1?.Dispose();
- this.reader2?.Dispose();
- }
-
- [Benchmark(Baseline = true)]
- public int StandardStreamReadByte()
- {
- int r = 0;
- Stream stream = this.stream1;
-
- for (int i = 0; i < stream.Length; i++)
- {
- r += stream.ReadByte();
- }
-
- return r;
- }
-
- [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 DoubleBufferedStreamReadByte()
- {
- int r = 0;
- DoubleBufferedStreamReader reader = this.reader1;
-
- for (int i = 0; i < reader.Length; i++)
- {
- r += reader.ReadByte();
- }
-
- 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 SimpleReadByte()
- {
- byte[] b = this.buffer;
- int r = 0;
- for (int i = 0; i < b.Length; i++)
- {
- r += b[i];
- }
-
- return r;
- }
-
- private static byte[] CreateTestBytes()
- {
- var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3];
- var random = new Random();
- random.NextBytes(buffer);
-
- return buffer;
- }
- }
-
- /* RESULTS (2019 April 24):
-
- BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17763.437 (1809/October2018Update/Redstone5)
- Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
- .NET Core SDK=2.2.202
- [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
- Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0
- Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
-
- IterationCount=3 LaunchCount=1 WarmupCount=3
-
- | Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
- |----------------------------- |----- |-------- |---------:|-----------:|----------:|------:|--------:|------:|------:|------:|----------:|
- | StandardStreamReadByte | Clr | Clr | 96.71 us | 5.9950 us | 0.3286 us | 1.00 | 0.00 | - | - | - | - |
- | StandardStreamRead | Clr | Clr | 77.73 us | 5.2284 us | 0.2866 us | 0.80 | 0.00 | - | - | - | - |
- | DoubleBufferedStreamReadByte | Clr | Clr | 23.17 us | 26.2354 us | 1.4381 us | 0.24 | 0.01 | - | - | - | - |
- | DoubleBufferedStreamRead | Clr | Clr | 33.35 us | 3.4071 us | 0.1868 us | 0.34 | 0.00 | - | - | - | - |
- | SimpleReadByte | Clr | Clr | 10.85 us | 0.4927 us | 0.0270 us | 0.11 | 0.00 | - | - | - | - |
- | | | | | | | | | | | | |
- | StandardStreamReadByte | Core | Core | 75.35 us | 12.9789 us | 0.7114 us | 1.00 | 0.00 | - | - | - | - |
- | StandardStreamRead | Core | Core | 55.36 us | 1.4432 us | 0.0791 us | 0.73 | 0.01 | - | - | - | - |
- | DoubleBufferedStreamReadByte | Core | Core | 21.47 us | 29.7076 us | 1.6284 us | 0.28 | 0.02 | - | - | - | - |
- | DoubleBufferedStreamRead | Core | Core | 29.67 us | 2.5988 us | 0.1424 us | 0.39 | 0.00 | - | - | - | - |
- | SimpleReadByte | Core | Core | 10.84 us | 0.7567 us | 0.0415 us | 0.14 | 0.00 | - | - | - | - |
- */
-}
diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs
new file mode 100644
index 0000000000..baabb4784b
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs
@@ -0,0 +1,279 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Benchmarks.IO
+{
+ ///
+ /// A readonly stream wrapper 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 BufferedReadStreamWrapper : 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;
+
+ // Index within our buffer, not reader position.
+ private int readBufferIndex;
+
+ // Matches what the stream position would be without buffering
+ private long readerPosition;
+
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The input stream.
+ public BufferedReadStreamWrapper(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 = 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 { get; }
+
+ ///
+ /// Gets or sets the current position within the stream.
+ ///
+ public long Position
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.readerPosition;
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ set
+ {
+ // Only reset readBufferIndex if we are out of bounds of our working buffer
+ // otherwise we should simply move the value by the diff.
+ if (this.IsInReadBuffer(value, out long index))
+ {
+ this.readBufferIndex = (int)index;
+ this.readerPosition = value;
+ }
+ else
+ {
+ // Base stream seek will throw for us if invalid.
+ this.stream.Seek(value, SeekOrigin.Begin);
+ this.readerPosition = value;
+ this.readBufferIndex = BufferLength;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ReadByte()
+ {
+ if (this.readerPosition >= this.Length)
+ {
+ 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 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 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 long Seek(long offset, SeekOrigin origin)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ this.Position = offset;
+ break;
+
+ case SeekOrigin.Current:
+ this.Position += offset;
+ break;
+
+ case SeekOrigin.End:
+ this.Position = this.Length - offset;
+ break;
+ }
+
+ return this.readerPosition;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.isDisposed = true;
+ this.readBufferHandle.Dispose();
+ ArrayPool.Shared.Return(this.readBuffer);
+ this.Flush();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsInReadBuffer(long newPosition, out long index)
+ {
+ index = newPosition - this.readerPosition + 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.AggressiveInlining)]
+ 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)
+ {
+ long n = this.Length - this.readerPosition;
+ if (n > count)
+ {
+ return count;
+ }
+
+ if (n < 0)
+ {
+ return 0;
+ }
+
+ return (int)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/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
new file mode 100644
index 0000000000..72cceae90e
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
@@ -0,0 +1,206 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using SixLabors.ImageSharp.IO;
+
+namespace SixLabors.ImageSharp.Benchmarks.IO
+{
+ [Config(typeof(Config.ShortClr))]
+ public class BufferedStreams
+ {
+ private readonly byte[] buffer = CreateTestBytes();
+ private readonly byte[] chunk1 = new byte[2];
+ private readonly byte[] chunk2 = new byte[2];
+
+ private MemoryStream stream1;
+ private MemoryStream stream2;
+ private MemoryStream stream3;
+ private MemoryStream stream4;
+ private MemoryStream stream5;
+ private MemoryStream stream6;
+ private BufferedReadStream bufferedStream1;
+ private BufferedReadStream bufferedStream2;
+ private BufferedReadStreamWrapper bufferedStreamWrap1;
+ private BufferedReadStreamWrapper bufferedStreamWrap2;
+
+ [GlobalSetup]
+ public void CreateStreams()
+ {
+ this.stream1 = new MemoryStream(this.buffer);
+ 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.bufferedStream1 = new BufferedReadStream(this.stream3);
+ this.bufferedStream2 = new BufferedReadStream(this.stream4);
+ this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5);
+ this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6);
+ }
+
+ [GlobalCleanup]
+ public void DestroyStreams()
+ {
+ this.bufferedStream1?.Dispose();
+ this.bufferedStream2?.Dispose();
+ this.bufferedStreamWrap1?.Dispose();
+ this.bufferedStreamWrap2?.Dispose();
+ this.stream1?.Dispose();
+ this.stream2?.Dispose();
+ this.stream3?.Dispose();
+ this.stream4?.Dispose();
+ this.stream5?.Dispose();
+ this.stream6?.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 BufferedReadStreamRead()
+ {
+ int r = 0;
+ BufferedReadStream reader = this.bufferedStream1;
+ byte[] b = this.chunk2;
+
+ for (int i = 0; i < reader.Length / 2; i++)
+ {
+ r += reader.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int BufferedReadStreamWrapRead()
+ {
+ int r = 0;
+ BufferedReadStreamWrapper reader = this.bufferedStreamWrap1;
+ byte[] b = this.chunk2;
+
+ for (int i = 0; i < reader.Length / 2; i++)
+ {
+ r += reader.Read(b, 0, 2);
+ }
+
+ return r;
+ }
+
+ [Benchmark(Baseline = true)]
+ public int StandardStreamReadByte()
+ {
+ int r = 0;
+ Stream stream = this.stream2;
+
+ for (int i = 0; i < stream.Length; i++)
+ {
+ r += stream.ReadByte();
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int BufferedReadStreamReadByte()
+ {
+ int r = 0;
+ BufferedReadStream reader = this.bufferedStream2;
+
+ for (int i = 0; i < reader.Length; i++)
+ {
+ r += reader.ReadByte();
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int BufferedReadStreamWrapReadByte()
+ {
+ int r = 0;
+ BufferedReadStreamWrapper reader = this.bufferedStreamWrap2;
+
+ for (int i = 0; i < reader.Length; i++)
+ {
+ r += reader.ReadByte();
+ }
+
+ return r;
+ }
+
+ [Benchmark]
+ public int ArrayReadByte()
+ {
+ byte[] b = this.buffer;
+ int r = 0;
+ for (int i = 0; i < b.Length; i++)
+ {
+ r += b[i];
+ }
+
+ return r;
+ }
+
+ private static byte[] CreateTestBytes()
+ {
+ var buffer = new byte[BufferedReadStream.BufferLength * 3];
+ var random = new Random();
+ random.NextBytes(buffer);
+
+ return buffer;
+ }
+ }
+
+ /*
+ BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19041
+ Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
+ .NET Core SDK=3.1.301
+ [Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
+ Job-LKLBOT : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT
+ Job-RSTMKF : .NET Core 2.1.19 (CoreCLR 4.6.28928.01, CoreFX 4.6.28928.04), X64 RyuJIT
+ Job-PZIHIV : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
+
+ IterationCount=3 LaunchCount=1 WarmupCount=3
+
+| Method | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
+|------------------------------- |-------------- |----------:|------------:|-----------:|------:|--------:|------:|------:|------:|----------:|
+| StandardStreamRead | .NET 4.7.2 | 63.238 us | 49.7827 us | 2.7288 us | 0.66 | 0.13 | - | - | - | - |
+| BufferedReadStreamRead | .NET 4.7.2 | 66.092 us | 0.4273 us | 0.0234 us | 0.69 | 0.11 | - | - | - | - |
+| BufferedReadStreamWrapRead | .NET 4.7.2 | 26.216 us | 3.0527 us | 0.1673 us | 0.27 | 0.04 | - | - | - | - |
+| StandardStreamReadByte | .NET 4.7.2 | 97.900 us | 261.7204 us | 14.3458 us | 1.00 | 0.00 | - | - | - | - |
+| BufferedReadStreamReadByte | .NET 4.7.2 | 97.260 us | 1.2979 us | 0.0711 us | 1.01 | 0.15 | - | - | - | - |
+| BufferedReadStreamWrapReadByte | .NET 4.7.2 | 19.170 us | 2.2296 us | 0.1222 us | 0.20 | 0.03 | - | - | - | - |
+| ArrayReadByte | .NET 4.7.2 | 12.878 us | 11.1292 us | 0.6100 us | 0.13 | 0.02 | - | - | - | - |
+| | | | | | | | | | | |
+| StandardStreamRead | .NET Core 2.1 | 60.618 us | 131.7038 us | 7.2191 us | 0.78 | 0.10 | - | - | - | - |
+| BufferedReadStreamRead | .NET Core 2.1 | 30.006 us | 25.2499 us | 1.3840 us | 0.38 | 0.02 | - | - | - | - |
+| BufferedReadStreamWrapRead | .NET Core 2.1 | 29.241 us | 6.5020 us | 0.3564 us | 0.37 | 0.01 | - | - | - | - |
+| StandardStreamReadByte | .NET Core 2.1 | 78.074 us | 15.8463 us | 0.8686 us | 1.00 | 0.00 | - | - | - | - |
+| BufferedReadStreamReadByte | .NET Core 2.1 | 14.737 us | 20.1510 us | 1.1045 us | 0.19 | 0.01 | - | - | - | - |
+| BufferedReadStreamWrapReadByte | .NET Core 2.1 | 13.234 us | 1.4711 us | 0.0806 us | 0.17 | 0.00 | - | - | - | - |
+| ArrayReadByte | .NET Core 2.1 | 9.373 us | 0.6108 us | 0.0335 us | 0.12 | 0.00 | - | - | - | - |
+| | | | | | | | | | | |
+| StandardStreamRead | .NET Core 3.1 | 52.151 us | 19.9456 us | 1.0933 us | 0.65 | 0.03 | - | - | - | - |
+| BufferedReadStreamRead | .NET Core 3.1 | 29.217 us | 0.2490 us | 0.0136 us | 0.36 | 0.01 | - | - | - | - |
+| BufferedReadStreamWrapRead | .NET Core 3.1 | 32.962 us | 7.1382 us | 0.3913 us | 0.41 | 0.02 | - | - | - | - |
+| StandardStreamReadByte | .NET Core 3.1 | 80.310 us | 45.0350 us | 2.4685 us | 1.00 | 0.00 | - | - | - | - |
+| BufferedReadStreamReadByte | .NET Core 3.1 | 13.092 us | 0.6268 us | 0.0344 us | 0.16 | 0.00 | - | - | - | - |
+| BufferedReadStreamWrapReadByte | .NET Core 3.1 | 13.282 us | 3.8689 us | 0.2121 us | 0.17 | 0.01 | - | - | - | - |
+| ArrayReadByte | .NET Core 3.1 | 9.349 us | 2.9860 us | 0.1637 us | 0.12 | 0.00 | - | - | - | - |
+ */
+}
diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
index 8c848fd049..eaab162ff2 100644
--- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
+++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
@@ -32,7 +32,6 @@
-
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index e69ba98f91..0694a08556 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
@@ -71,16 +72,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public void ParseStream_BasicPropertiesAreCorrect()
{
byte[] bytes = TestFile.Create(TestImages.Jpeg.Progressive.Progress).Bytes;
- using (var ms = new MemoryStream(bytes))
- {
- var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
- decoder.ParseStream(ms);
-
- // I don't know why these numbers are different. All I know is that the decoder works
- // and spectral data is exactly correct also.
- // VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31);
- VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 44, 62, 22, 31, 22, 31);
- }
+ using var ms = new MemoryStream(bytes);
+ using var bufferedStream = new BufferedReadStream(ms);
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
+ decoder.ParseStream(bufferedStream);
+
+ // I don't know why these numbers are different. All I know is that the decoder works
+ // and spectral data is exactly correct also.
+ // VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31);
+ VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 44, 62, 22, 31, 22, 31);
}
public const string DecodeBaselineJpegOutputName = "DecodeBaselineJpeg";
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
index fad2f06b14..1d200592a8 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
@@ -51,13 +52,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
- using (var ms = new MemoryStream(sourceBytes))
- {
- decoder.ParseStream(ms);
+ using var ms = new MemoryStream(sourceBytes);
+ using var bufferedStream = new BufferedReadStream(ms);
+ decoder.ParseStream(bufferedStream);
- var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
- VerifyJpeg.SaveSpectralImage(provider, data);
- }
+ var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
+ VerifyJpeg.SaveSpectralImage(provider, data);
}
[Theory]
@@ -74,13 +74,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
- using (var ms = new MemoryStream(sourceBytes))
- {
- decoder.ParseStream(ms);
- var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
+ using var ms = new MemoryStream(sourceBytes);
+ using var bufferedStream = new BufferedReadStream(ms);
+ decoder.ParseStream(bufferedStream);
- this.VerifySpectralCorrectnessImpl(provider, imageSharpData);
- }
+ var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
+ this.VerifySpectralCorrectnessImpl(provider, imageSharpData);
}
private void VerifySpectralCorrectnessImpl(
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
index 983faddf1c..96d85fd8e4 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
@@ -8,7 +8,7 @@ using System.Text;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
-
+using SixLabors.ImageSharp.IO;
using Xunit;
using Xunit.Abstractions;
@@ -192,12 +192,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
internal static JpegDecoderCore ParseJpegStream(string testFileName, bool metaDataOnly = false)
{
byte[] bytes = TestFile.Create(testFileName).Bytes;
- using (var ms = new MemoryStream(bytes))
- {
- var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
- decoder.ParseStream(ms, metaDataOnly);
- return decoder;
- }
+ using var ms = new MemoryStream(bytes);
+ using var bufferedStream = new BufferedReadStream(ms);
+
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
+ decoder.ParseStream(bufferedStream, metaDataOnly);
+
+ return decoder;
}
}
}
diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
new file mode 100644
index 0000000000..d08d5adef4
--- /dev/null
+++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
@@ -0,0 +1,278 @@
+// Copyright (c) Six Labors.
+// 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 BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ Span 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);
+ }
+ }
+
+ [Fact]
+ public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin()
+ {
+ using (MemoryStream stream = this.CreateTestStream())
+ {
+ byte[] expected = stream.ToArray();
+ using (var reader = new BufferedReadStream(stream))
+ {
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], reader.ReadByte());
+ }
+ }
+ }
+ }
+
+ private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3)
+ {
+ var buffer = new byte[length];
+ var random = new Random();
+ random.NextBytes(buffer);
+
+ return new EvilStream(buffer);
+ }
+
+ // Simulates a stream that can only return 1 byte at a time per read instruction.
+ // See https://github.com/SixLabors/ImageSharp/issues/1268
+ private class EvilStream : MemoryStream
+ {
+ public EvilStream(byte[] buffer)
+ : base(buffer)
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return base.Read(buffer, offset, 1);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs b/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs
deleted file mode 100644
index 73010fe2ca..0000000000
--- a/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.IO;
-using SixLabors.ImageSharp.IO;
-using SixLabors.ImageSharp.Memory;
-using Xunit;
-
-namespace SixLabors.ImageSharp.Tests.IO
-{
- public class DoubleBufferedStreamReaderTests
- {
- private readonly MemoryAllocator allocator = Configuration.Default.MemoryAllocator;
-
- [Fact]
- public void DoubleBufferedStreamReaderCanReadSingleByteFromOrigin()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- byte[] expected = stream.ToArray();
- var reader = new DoubleBufferedStreamReader(this.allocator, stream);
-
- Assert.Equal(expected[0], reader.ReadByte());
-
- // We've read a whole chunk but increment by 1 in our reader.
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength);
- Assert.Equal(1, reader.Position);
- }
- }
-
- [Fact]
- public void DoubleBufferedStreamReaderCanReadSingleByteFromOffset()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- byte[] expected = stream.ToArray();
- const int offset = 5;
- var reader = new DoubleBufferedStreamReader(this.allocator, 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(stream.Position, DoubleBufferedStreamReader.ChunkLength + offset);
- Assert.Equal(offset + 1, reader.Position);
- }
- }
-
- [Fact]
- public void DoubleBufferedStreamReaderCanReadSubsequentSingleByteCorrectly()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- byte[] expected = stream.ToArray();
- var reader = new DoubleBufferedStreamReader(this.allocator, stream);
-
- for (int i = 0; i < expected.Length; i++)
- {
- Assert.Equal(expected[i], reader.ReadByte());
- Assert.Equal(i + 1, reader.Position);
-
- if (i < DoubleBufferedStreamReader.ChunkLength)
- {
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength);
- }
- else if (i >= DoubleBufferedStreamReader.ChunkLength && i < DoubleBufferedStreamReader.ChunkLength * 2)
- {
- // We should have advanced to the second chunk now.
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2);
- }
- else
- {
- // We should have advanced to the third chunk now.
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3);
- }
- }
- }
- }
-
- [Fact]
- public void DoubleBufferedStreamReaderCanReadMultipleBytesFromOrigin()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- var buffer = new byte[2];
- byte[] expected = stream.ToArray();
- var reader = new DoubleBufferedStreamReader(this.allocator, 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, DoubleBufferedStreamReader.ChunkLength);
- Assert.Equal(buffer.Length, reader.Position);
- }
- }
-
- [Fact]
- public void DoubleBufferedStreamReaderCanReadSubsequentMultipleByteCorrectly()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- var buffer = new byte[2];
- byte[] expected = stream.ToArray();
- var reader = new DoubleBufferedStreamReader(this.allocator, 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 < DoubleBufferedStreamReader.ChunkLength)
- {
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength);
- }
- else if (offset >= DoubleBufferedStreamReader.ChunkLength && offset < DoubleBufferedStreamReader.ChunkLength * 2)
- {
- // We should have advanced to the second chunk now.
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2);
- }
- else
- {
- // We should have advanced to the third chunk now.
- Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3);
- }
- }
- }
- }
-
- [Fact]
- public void DoubleBufferedStreamReaderCanSkip()
- {
- using (MemoryStream stream = this.CreateTestStream())
- {
- byte[] expected = stream.ToArray();
- var reader = new DoubleBufferedStreamReader(this.allocator, stream);
-
- int skip = 50;
- int plusOne = 1;
- int skip2 = DoubleBufferedStreamReader.ChunkLength;
-
- // 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());
- }
- }
-
- private MemoryStream CreateTestStream()
- {
- var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3];
- var random = new Random();
- random.NextBytes(buffer);
-
- return new MemoryStream(buffer);
- }
- }
-}
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs
index 7478f76f08..9d4ffdace7 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs
@@ -81,9 +81,9 @@ namespace SixLabors.ImageSharp.Tests
{
Assert.Throws(
() =>
- {
- Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString());
- });
+ {
+ Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString());
+ });
}
[Fact]
@@ -91,9 +91,9 @@ namespace SixLabors.ImageSharp.Tests
{
Assert.Throws(
() =>
- {
- Image.Load(this.TopLevelConfiguration, (string)null);
- });
+ {
+ Image.Load(this.TopLevelConfiguration, (string)null);
+ });
}
}
}