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 @@ public static int Read(this Stream stream, Span buffer, int offset, int co /// 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 @@ public static void Skip(this Stream stream, int count) 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 @@ public static void Skip(this Stream stream, int count) 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 @@ public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions, IImageInfoDe 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 @@ public async Task> DecodeAsync(Configuration configuration 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 @@ public Image Decode(Configuration configuration, Stream stream) 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 @@ public Task IdentifyAsync(Configuration configuration, Stream stream { 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 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private Stream stream; + private BufferedReadStream stream; /// /// The metadata. @@ -120,7 +118,7 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options) 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 @@ public Image Decode(Stream stream) } /// - 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 @@ private void ReadFileHeader() /// /// 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 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe 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 @@ public Task IdentifyAsync(Configuration configuration, Stream stream 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 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// /// The currently loaded stream. /// - private Stream stream; + private BufferedReadStream stream; /// /// The global color table. @@ -97,7 +97,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) 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 @@ public Image Decode(Stream stream) } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { try { @@ -572,7 +572,7 @@ private void SetFrameMetadata(ImageFrameMetadata meta) /// 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 @@ internal sealed class LzwDecoder : IDisposable /// /// The stream to decode. /// - private readonly Stream stream; + private readonly BufferedReadStream stream; /// /// The prefix buffer. @@ -52,8 +51,8 @@ internal sealed class LzwDecoder : IDisposable /// /// 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 @@ internal interface IImageDecoderInternals /// /// 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 @@ internal static class ImageDecoderUtilities /// 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 @@ internal struct HuffmanScanBuffer // 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.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 @@ internal class HuffmanScanDecoder 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 @@ internal class HuffmanScanDecoder /// 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 @@ public async Task> DecodeAsync(Configuration configuration 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 @@ public Image Decode(Configuration configuration, Stream stream) 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 @@ public IImageInfo Identify(Configuration configuration, Stream stream) { 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 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) /// 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 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) /// 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 @@ public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStr } /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { this.ParseStream(stream); @@ -224,7 +217,7 @@ public Image Decode(Stream stream) } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { this.ParseStream(stream, true); this.InitExifProfile(); @@ -240,22 +233,21 @@ public IImageInfo Identify(Stream stream) /// /// 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 @@ public void ParseStream(Stream stream, bool metadataOnly = false) 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 @@ public void ParseStream(Stream stream, bool metadataOnly = false) 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 @@ public void ParseStream(Stream stream, bool metadataOnly = false) 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 @@ private void ExtendProfile(ref byte[] profile, byte[] extension) /// /// 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 @@ private void ProcessApplicationHeaderMarker(int remaining) // 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 @@ private void ProcessApp1Marker(int remaining) /// /// 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 @@ private void ProcessApp2Marker(int remaining) 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 @@ private void ProcessApp2Marker(int remaining) /// 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 @@ private static int ReadResourceDataLength(Span blockDataSpan, int resource /// 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 @@ private void ProcessDefineQuantizationTablesMarker(int remaining) 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 @@ private void ProcessDefineQuantizationTablesMarker(int remaining) 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 @@ private void ProcessDefineQuantizationTablesMarker(int remaining) /// /// 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 @@ private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMar // 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 @@ private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMar 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 @@ private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMar /// 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 @@ private void ProcessDefineHuffmanTablesMarker(int remaining) 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 @@ private void ProcessDefineHuffmanTablesMarker(int remaining) 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 @@ private void ProcessDefineHuffmanTablesMarker(int remaining) 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 @@ private void ProcessDefineHuffmanTablesMarker(int remaining) /// 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 @@ private void ProcessStartOfScanMarker() } 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 @@ private void BuildHuffmanTable(HuffmanTable[] tables, int index, ReadOnlySpan /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private Stream currentStream; + private BufferedReadStream currentStream; /// /// The png header. @@ -132,7 +132,7 @@ public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) 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 @@ public Image Decode(Stream stream) } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); @@ -499,7 +499,7 @@ private void ReadScanlines(PngChunk chunk, ImageFrame image, 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 @@ private void DecodePixelData(Stream compressedStream, ImageFrame /// 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 @@ private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan 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 @@ internal sealed class ZlibInflateStream : Stream /// /// 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 @@ internal sealed class ZlibInflateStream : Stream /// 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 @@ public ZlibInflateStream(Stream innerStream) /// /// 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 @@ private bool InitializeInflateStream(bool isCriticalChunk) 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 @@ public async Task> DecodeAsync(Configuration configuration 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 @@ public Image Decode(Configuration configuration, Stream stream) 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 @@ 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) => 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 @@ public Task IdentifyAsync(Configuration configuration, Stream stream { 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 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private Stream currentStream; + private BufferedReadStream currentStream; /// /// The bitmap decoder options. @@ -78,7 +77,7 @@ public TgaDecoderCore(Configuration configuration, ITgaDecoderOptions options) 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 @@ private void ReadRle(int width, int height, Buffer2D pixels, int } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { this.ReadFileHeader(stream); return new ImageInfo( @@ -868,9 +867,9 @@ private static bool InvertX(TgaImageOrigin origin) /// /// 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 @@ private static IImageFormat InternalDetectFormat(Stream stream, Configuration co 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.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 @@ public static Image Load(Configuration configuration, Stream stream, out IImageF 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 @@ private static T WithSeekableStream(Configuration configuration, Stream strea } // 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 @@ private static async Task WithSeekableStreamAsync( 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 @@ public void Save(Stream stream, IImageEncoder encoder) /// /// 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 @@ public EncodeVisitor(IImageEncoder encoder, Stream stream) 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 BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Tests; using SDSize = System.Drawing.Size; @@ -30,24 +31,20 @@ public void Setup() [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 @@ public class ShortClr : Benchmarks.Config 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 @@ public ShortClr() 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.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 @@ public JpegDecoderTests(ITestOutputHelper output) 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.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 @@ public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider(TestImageProvider provider 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 SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; - +using SixLabors.ImageSharp.IO; using Xunit; using Xunit.Abstractions; @@ -192,12 +192,13 @@ internal void CompareBlocks(Span a, Span b, float tolerance) 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 @@ public void WhenFileNotFound_Throws() { Assert.Throws( () => - { - Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString()); - }); + { + Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString()); + }); } [Fact] @@ -91,9 +91,9 @@ public void WhenPathIsNull_Throws() { Assert.Throws( () => - { - Image.Load(this.TopLevelConfiguration, (string)null); - }); + { + Image.Load(this.TopLevelConfiguration, (string)null); + }); } } }