diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d45d98b393..89d1a75f27 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# How to contribute to ImageSharp +# How to contribute to SixLabors.ImageSharp #### **Did you find a bug?** @@ -12,14 +12,20 @@ * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. -* Before submitting, please ensure that your code matches the existing coding patterns and practise as demonstrated in the repository. These follow strict Stylecop rules :cop:. +* Before submitting, please ensure that your code matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules :cop:. #### **Do you intend to add a new feature or change an existing one?** -* Suggest your change in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General) and start writing code. +* Suggest your change in the [Ideas Discussions Channel](https://github.com/SixLabors/ImageSharp/discussions?discussions_q=category%3AIdeas) and start writing code. * Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. +#### **Building** + + * When first cloning the repo, make sure to run `git submodule update --init --recursive` otherwise the submodules (e.g. `shared-infrastructure`) will be missing. + + * Run `dotnet build` in the root of the repo, or open the ImageSharp.sln file in Visual Studio and build from there. + #### **Running tests and Debugging** * Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/master/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules! @@ -27,14 +33,12 @@ #### **Do you have questions about consuming the library or the source code?** -* Ask any question about how to use ImageSharp in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General). +* Ask any question about how to use SixLabors.ImageSharp in the [Help Discussions Channel](https://github.com/SixLabors/ImageSharp/discussions?discussions_q=category%3AHelp). #### Code of Conduct This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). -And please remember. ImageSharp is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible imageprocessing available to all. Open Source can only exist with your help. +And please remember. SixLabors.ImageSharp is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible image processing available to all. Open Source can only exist with your help. Thanks for reading! - -James Jackson-South :heart: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5a9d1dde09..cf9f787526 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Ask a Question - url: https://github.com/SixLabors/ImageSharp/discussions/new?category_id=6331980 + url: https://github.com/SixLabors/ImageSharp/discussions?discussions_q=category%3AHelp about: Ask a question about this project. - name: Feature Request - url: https://github.com/SixLabors/ImageSharp/discussions/new?category_id=6331981 + url: https://github.com/SixLabors/ImageSharp/discussions?discussions_q=category%3AIdeas about: Share ideas for new features for this project. diff --git a/Directory.Build.targets b/Directory.Build.targets index cc14bbdbf7..4e7ab9e6b7 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -25,12 +25,16 @@ - + + - + diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 637751f6e4..f2367d488a 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -2,11 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.IO; using SixLabors.ImageSharp.Memory; -#if !SUPPORTS_SPAN_STREAM -using System.Buffers; -#endif namespace SixLabors.ImageSharp { @@ -40,7 +38,7 @@ namespace SixLabors.ImageSharp /// Skips the number of bytes in the given stream. /// /// The stream. - /// The count. + /// A byte offset relative to the origin parameter. public static void Skip(this Stream stream, int count) { if (count < 1) @@ -50,14 +48,16 @@ namespace SixLabors.ImageSharp if (stream.CanSeek) { - stream.Seek(count, SeekOrigin.Current); // Position += count; + stream.Seek(count, SeekOrigin.Current); + return; } - else + + byte[] buffer = ArrayPool.Shared.Rent(count); + try { - var foo = new byte[count]; while (count > 0) { - int bytesRead = stream.Read(foo, 0, count); + int bytesRead = stream.Read(buffer, 0, count); if (bytesRead == 0) { break; @@ -66,6 +66,10 @@ namespace SixLabors.ImageSharp count -= bytesRead; } } + finally + { + ArrayPool.Shared.Return(buffer); + } } public static void Read(this Stream stream, IManagedByteBuffer buffer) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 7e0a263766..eea6996e1b 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Net.Http; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; @@ -27,7 +26,8 @@ namespace SixLabors.ImageSharp /// A lazily initialized configuration default instance. /// private static readonly Lazy Lazy = new Lazy(CreateDefaultInstance); - + private const int DefaultStreamProcessingBufferSize = 8096; + private int streamProcessingBufferSize = DefaultStreamProcessingBufferSize; private int maxDegreeOfParallelism = Environment.ProcessorCount; /// @@ -76,6 +76,24 @@ namespace SixLabors.ImageSharp } } + /// + /// Gets or sets the size of the buffer to use when working with streams. + /// Intitialized with by default. + /// + public int StreamProcessingBufferSize + { + get => this.streamProcessingBufferSize; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(this.StreamProcessingBufferSize)); + } + + this.streamProcessingBufferSize = value; + } + } + /// /// Gets a set of properties for the Congiguration. /// @@ -146,6 +164,7 @@ namespace SixLabors.ImageSharp return new Configuration { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, + StreamProcessingBufferSize = this.StreamProcessingBufferSize, ImageFormatsManager = this.ImageFormatsManager, MemoryAllocator = this.MemoryAllocator, ImageOperationsProvider = this.ImageOperationsProvider, diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 16da086c9e..cb26ff606a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -29,8 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black; /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) - where TPixel : unmanaged, IPixel + public Image Decode(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -38,19 +39,24 @@ namespace SixLabors.ImageSharp.Formats.Bmp try { - return await decoder.DecodeAsync(stream).ConfigureAwait(false); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) { Size dims = decoder.Dimensions; - throw new InvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); + throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); } } + /// + public Image Decode(Configuration configuration, Stream stream) + => this.Decode(configuration, stream); + /// - public Image Decode(Configuration configuration, Stream stream) - where TPixel : unmanaged, IPixel + public async Task> DecodeAsync(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -58,28 +64,28 @@ namespace SixLabors.ImageSharp.Formats.Bmp try { - return decoder.Decode(stream); + using var bufferedStream = new BufferedReadStream(configuration, 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(configuration, stream); + return new BmpDecoderCore(configuration, this).Identify(bufferedStream); } /// @@ -87,7 +93,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp { Guard.NotNull(stream, nameof(stream)); - return new BmpDecoderCore(configuration, this).IdentifyAsync(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return new BmpDecoderCore(configuration, this).IdentifyAsync(bufferedStream); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 4b14061cf8..ea8fd11a86 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -4,10 +4,8 @@ using System; using System.Buffers; using System.Buffers.Binary; -using System.IO; using System.Numerics; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -62,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// The stream to decode from. /// - private Stream stream; + private BufferedReadStream stream; /// /// The metadata. @@ -120,7 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { try @@ -199,7 +197,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata); @@ -1355,7 +1353,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps /// the bytes per color palette entry's can be 3 bytes instead of 4. - private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palette) + private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette) { this.stream = stream; diff --git a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs b/src/ImageSharp/Formats/Bmp/ImageExtensions.cs index 93e2b3fb19..8d97c8b464 100644 --- a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Bmp/ImageExtensions.cs @@ -1,8 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Bmp; @@ -13,13 +13,64 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null); + + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null); + + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsBmp(this Image source, Stream stream) => SaveAsBmp(source, stream, null); + /// /// Saves the image to the given stream with the bmp format. /// /// The image this method extends. /// The stream to save the image to. /// Thrown if the stream is null. - public static void SaveAsBmp(this Image source, Stream stream) => source.SaveAsBmp(stream, null); + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream) => SaveAsBmpAsync(source, stream, null); /// /// Saves the image to the given stream with the bmp format. @@ -32,5 +83,18 @@ namespace SixLabors.ImageSharp source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 5f4fdd0fa6..2b7103072b 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.IO; using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -26,54 +26,66 @@ namespace SixLabors.ImageSharp.Formats.Gif public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); try { - return await decoder.DecodeAsync(stream).ConfigureAwait(false); + using var bufferedStream = new BufferedReadStream(configuration, 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(configuration, 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(configuration, stream); + return decoder.Identify(bufferedStream); } /// @@ -82,13 +94,9 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(stream, nameof(stream)); var decoder = new GifDecoderCore(configuration, this); - return decoder.IdentifyAsync(stream); - } - /// - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return decoder.IdentifyAsync(bufferedStream); + } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e4c98799ba..78ffee8bdb 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The currently loaded stream. /// - private Stream stream; + private BufferedReadStream stream; /// /// The global color table. @@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator; /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { Image image = null; @@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { try { @@ -572,7 +572,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Reads the logical screen descriptor and global color table blocks /// /// The stream containing image data. - private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream) + private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream) { this.stream = stream; diff --git a/src/ImageSharp/Formats/Gif/ImageExtensions.cs b/src/ImageSharp/Formats/Gif/ImageExtensions.cs index 7e762d68b8..d262b056cd 100644 --- a/src/ImageSharp/Formats/Gif/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Gif/ImageExtensions.cs @@ -1,8 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Gif; @@ -14,23 +14,87 @@ namespace SixLabors.ImageSharp public static partial class ImageExtensions { /// - /// Saves the image to the given stream in the gif format. + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null); + + /// + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null); + + /// + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsGif(this Image source, Stream stream) => SaveAsGif(source, stream, null); + + /// + /// Saves the image to the given stream with the gif format. /// /// The image this method extends. /// The stream to save the image to. /// Thrown if the stream is null. - public static void SaveAsGif(this Image source, Stream stream) => source.SaveAsGif(stream, null); + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream) => SaveAsGifAsync(source, stream, null); /// - /// Saves the image to the given stream in the gif format. + /// Saves the image to the given stream with the gif format. /// /// The image this method extends. /// The stream to save the image to. - /// The options for the encoder. + /// The encoder to save the image with. /// Thrown if the stream is null. public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index 6a975951c4..9eaa55566b 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -3,10 +3,9 @@ using System; using System.Buffers; -using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif @@ -29,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The stream to decode. /// - private readonly Stream stream; + private readonly BufferedReadStream stream; /// /// The prefix buffer. @@ -52,8 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The to use for buffer allocations. /// The stream to read from. - /// is null. - public LzwDecoder(MemoryAllocator memoryAllocator, Stream stream) + /// is null. + public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs index 3ab9123530..33748bf245 100644 --- a/src/ImageSharp/Formats/IImageDecoderInternals.cs +++ b/src/ImageSharp/Formats/IImageDecoderInternals.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.IO; +using System; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -21,18 +22,16 @@ namespace SixLabors.ImageSharp.Formats /// /// The pixel format. /// The stream, where the image should be decoded from. Cannot be null. - /// - /// is null. - /// + /// is null. /// The decoded image. - Image Decode(Stream stream) + Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel; /// /// Reads the raw image information from the specified stream. /// - /// The containing image data. + /// The containing image data. /// The . - IImageInfo Identify(Stream stream); + IImageInfo Identify(BufferedReadStream stream); } } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 617b4b8ce6..9d1639a090 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.IO; +using System; using System.Threading.Tasks; -using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -14,42 +14,21 @@ namespace SixLabors.ImageSharp.Formats /// Reads the raw image information from the specified stream. /// /// The decoder. - /// The containing image data. - public static async Task IdentifyAsync(this IImageDecoderInternals decoder, Stream stream) - { - if (stream.CanSeek) - { - return decoder.Identify(stream); - } - - using MemoryStream ms = decoder.Configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return decoder.Identify(ms); - } + /// The containing image data. + /// is null. + /// A representing the asynchronous operation. + public static Task IdentifyAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) + => Task.FromResult(decoder.Identify(stream)); /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The decoder. - /// The stream, where the image should be decoded from. Cannot be null. - /// - /// is null. - /// - /// The decoded image. - public static async Task> DecodeAsync(this IImageDecoderInternals decoder, Stream stream) + /// The containing image data. + /// A representing the asynchronous operation. + public static Task> DecodeAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - return decoder.Decode(stream); - } - - using MemoryStream ms = decoder.Configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); - await stream.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - return decoder.Decode(ms); - } + => Task.FromResult(decoder.Decode(stream)); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs index 76d5a2dd9a..7747801700 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal struct HuffmanScanBuffer { - private readonly DoubleBufferedStreamReader stream; + private readonly BufferedReadStream stream; // The entropy encoded code buffer. private ulong data; @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // Whether there is no more good data to pull from the stream for the current mcu. private bool badData; - public HuffmanScanBuffer(DoubleBufferedStreamReader stream) + public HuffmanScanBuffer(BufferedReadStream stream) { this.stream = stream; this.data = 0ul; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 10c1b9bcf4..d6c16f8260 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -5,7 +5,6 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private readonly JpegFrame frame; private readonly HuffmanTable[] dcHuffmanTables; private readonly HuffmanTable[] acHuffmanTables; - private readonly DoubleBufferedStreamReader stream; + private readonly BufferedReadStream stream; private readonly JpegComponent[] components; // The restart interval. @@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The successive approximation bit high end. /// The successive approximation bit low end. public HuffmanScanDecoder( - DoubleBufferedStreamReader stream, + BufferedReadStream stream, JpegFrame frame, HuffmanTable[] dcHuffmanTables, HuffmanTable[] acHuffmanTables, diff --git a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs index ee47aa345b..d6600b6253 100644 --- a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs @@ -1,8 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Jpeg; @@ -13,6 +13,48 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null); + + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null); + + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + /// /// Saves the image to the given stream with the jpeg format. /// @@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp /// /// The image this method extends. /// The stream to save the image to. - /// The options for the encoder. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream) => SaveAsJpegAsync(source, stream, null); + + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. /// Thrown if the stream is null. public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 2d2d7fb56e..3eaf3a4c47 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -13,13 +14,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfoDetector { - /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. - /// + /// public bool IgnoreMetadata { get; set; } /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -27,21 +26,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var decoder = new JpegDecoderCore(configuration, this); try { - return await decoder.DecodeAsync(stream).ConfigureAwait(false); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) { (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - JpegThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); + JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); // Not reachable, as the previous statement will throw a exception. return null; } } + /// + public Image Decode(Configuration configuration, Stream stream) + => this.Decode(configuration, stream); + /// - public Image Decode(Configuration configuration, Stream stream) + public async Task> DecodeAsync(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -49,23 +53,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var decoder = new JpegDecoderCore(configuration, this); try { - return decoder.Decode(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) { (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - JpegThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); + JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); // Not reachable, as the previous statement will throw a exception. return null; } } - /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); - /// public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); @@ -75,21 +76,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { Guard.NotNull(stream, nameof(stream)); - using (var decoder = new JpegDecoderCore(configuration, this)) - { - return decoder.Identify(stream); - } + using var decoder = new JpegDecoderCore(configuration, this); + using var bufferedStream = new BufferedReadStream(configuration, 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(configuration, stream); + + return decoder.IdentifyAsync(bufferedStream); } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index b4f37cd7f2..c0b09c4c2b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -3,10 +3,8 @@ using System; using System.Buffers.Binary; -using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -139,11 +137,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int BitsPerPixel => this.ComponentCount * this.Frame.Precision; - /// - /// Gets the input stream. - /// - public DoubleBufferedStreamReader InputStream { get; private set; } - /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// @@ -180,7 +173,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The buffer to read file markers to /// The input stream /// The - public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStreamReader stream) + public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream) { int value = stream.Read(marker, 0, 2); @@ -212,7 +205,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { this.ParseStream(stream); @@ -224,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { this.ParseStream(stream, true); this.InitExifProfile(); @@ -240,22 +233,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The input stream /// Whether to decode metadata only. - public void ParseStream(Stream stream, bool metadataOnly = false) + public void ParseStream(BufferedReadStream stream, bool metadataOnly = false) { this.Metadata = new ImageMetadata(); - this.InputStream = new DoubleBufferedStreamReader(this.Configuration.MemoryAllocator, stream); // Check for the Start Of Image marker. - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); } - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); byte marker = this.markerBuffer[1]; - fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2); + fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); this.QuantizationTables = new Block8x8F[4]; // Only assign what we need @@ -274,20 +266,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { // Get the marker length - int remaining = this.ReadUint16() - 2; + int remaining = this.ReadUint16(stream) - 2; switch (fileMarker.Marker) { case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(remaining, fileMarker, metadataOnly); + this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly); break; case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(); + this.ProcessStartOfScanMarker(stream); break; } else @@ -301,41 +293,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (metadataOnly) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } else { - this.ProcessDefineHuffmanTablesMarker(remaining); + this.ProcessDefineHuffmanTablesMarker(stream, remaining); } break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(remaining); + this.ProcessDefineQuantizationTablesMarker(stream, remaining); break; case JpegConstants.Markers.DRI: if (metadataOnly) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } else { - this.ProcessDefineRestartIntervalMarker(remaining); + this.ProcessDefineRestartIntervalMarker(stream, remaining); } break; case JpegConstants.Markers.APP0: - this.ProcessApplicationHeaderMarker(remaining); + this.ProcessApplicationHeaderMarker(stream, remaining); break; case JpegConstants.Markers.APP1: - this.ProcessApp1Marker(remaining); + this.ProcessApp1Marker(stream, remaining); break; case JpegConstants.Markers.APP2: - this.ProcessApp2Marker(remaining); + this.ProcessApp2Marker(stream, remaining); break; case JpegConstants.Markers.APP3: @@ -348,37 +340,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: - this.InputStream.Skip(remaining); + stream.Skip(remaining); break; case JpegConstants.Markers.APP13: - this.ProcessApp13Marker(remaining); + this.ProcessApp13Marker(stream, remaining); break; case JpegConstants.Markers.APP14: - this.ProcessApp14Marker(remaining); + this.ProcessApp14Marker(stream, remaining); break; case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: - this.InputStream.Skip(remaining); + stream.Skip(remaining); break; } } // Read on. - fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); + fileMarker = FindNextFileMarker(this.markerBuffer, stream); } } /// public void Dispose() { - this.InputStream?.Dispose(); this.Frame?.Dispose(); // Set large fields to null. - this.InputStream = null; this.Frame = null; this.dcHuffmanTables = null; this.acHuffmanTables = null; @@ -504,18 +494,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the application header containing the JFIF identifier plus extra data. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApplicationHeaderMarker(int remaining) + private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) { // We can only decode JFif identifiers. if (remaining < JFifMarker.Length) { // Skip the application header length - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } - this.InputStream.Read(this.temp, 0, JFifMarker.Length); + stream.Read(this.temp, 0, JFifMarker.Length); remaining -= JFifMarker.Length; JFifMarker.TryParse(this.temp, out this.jFif); @@ -523,26 +514,37 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // TODO: thumbnail if (remaining > 0) { - this.InputStream.Skip(remaining); + if (stream.Position + remaining >= stream.Length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad App0 Marker length."); + } + + 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; } + if (stream.Position + remaining >= stream.Length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); + } + var profile = new byte[remaining]; - this.InputStream.Read(profile, 0, remaining); + stream.Read(profile, 0, remaining); if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker)) { @@ -563,26 +565,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the App2 marker retrieving any stored ICC profile information /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp2Marker(int remaining) + private void ProcessApp2Marker(BufferedReadStream stream, int remaining) { // Length is 14 though we only need to check 12. const int Icclength = 14; if (remaining < Icclength || this.IgnoreMetadata) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } var identifier = new byte[Icclength]; - this.InputStream.Read(identifier, 0, Icclength); + stream.Read(identifier, 0, Icclength); remaining -= Icclength; // We have read it by this point if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) { this.isIcc = true; var profile = new byte[remaining]; - this.InputStream.Read(profile, 0, remaining); + stream.Read(profile, 0, remaining); if (this.iccData is null) { @@ -597,7 +600,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg else { // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this. - this.InputStream.Skip(remaining); + stream.Skip(remaining); } } @@ -605,21 +608,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp13Marker(int remaining) + private void ProcessApp13Marker(BufferedReadStream stream, int remaining) { if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } - this.InputStream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); + stream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length; if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker)) { var resourceBlockData = new byte[remaining]; - this.InputStream.Read(resourceBlockData, 0, remaining); + stream.Read(resourceBlockData, 0, remaining); Span blockDataSpan = resourceBlockData.AsSpan(); while (blockDataSpan.Length > 12) @@ -694,42 +698,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes the application header containing the Adobe identifier /// which stores image encoding information for DCT filters. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp14Marker(int remaining) + private void ProcessApp14Marker(BufferedReadStream stream, int remaining) { const int MarkerLength = AdobeMarker.Length; if (remaining < MarkerLength) { // Skip the application header length - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } - this.InputStream.Read(this.temp, 0, MarkerLength); + stream.Read(this.temp, 0, MarkerLength); remaining -= MarkerLength; AdobeMarker.TryParse(this.temp, out this.adobe); if (remaining > 0) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } } /// /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. /// + /// The input stream. /// The remaining bytes in the segment block. /// /// Thrown if the tables do not match the header /// - private void ProcessDefineQuantizationTablesMarker(int remaining) + private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { while (remaining > 0) { bool done = false; remaining--; - int quantizationTableSpec = this.InputStream.ReadByte(); + int quantizationTableSpec = stream.ReadByte(); int tableIndex = quantizationTableSpec & 15; // Max index. 4 Tables max. @@ -749,7 +755,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } - this.InputStream.Read(this.temp, 0, 64); + stream.Read(this.temp, 0, 64); remaining -= 64; ref Block8x8F table = ref this.QuantizationTables[tableIndex]; @@ -769,7 +775,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } - this.InputStream.Read(this.temp, 0, 128); + stream.Read(this.temp, 0, 128); remaining -= 128; ref Block8x8F table = ref this.QuantizationTables[tableIndex]; @@ -805,10 +811,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the Start of Frame marker. Specified in section B.2.2. /// + /// The input stream. /// The remaining bytes in the segment block. /// The current frame marker. /// Whether to parse metadata only - private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMarker, bool metadataOnly) + private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly) { if (this.Frame != null) { @@ -822,7 +829,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Read initial marker definitions. const int length = 6; - this.InputStream.Read(this.temp, 0, length); + stream.Read(this.temp, 0, length); // We only support 8-bit and 12-bit precision. if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1) @@ -860,7 +867,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowBadMarker("SOFn", remaining); } - this.InputStream.Read(this.temp, 0, remaining); + stream.Read(this.temp, 0, remaining); // No need to pool this. They max out at 4 this.Frame.ComponentIds = new byte[this.ComponentCount]; @@ -907,8 +914,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes a Define Huffman Table marker, and initializes a huffman /// struct from its contents. Specified in section B.2.4.2. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessDefineHuffmanTablesMarker(int remaining) + private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) { int length = remaining; @@ -917,7 +925,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanData.GetSpan()); for (int i = 2; i < remaining;) { - byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); + byte huffmanTableSpec = (byte)stream.ReadByte(); int tableType = huffmanTableSpec >> 4; int tableIndex = huffmanTableSpec & 15; @@ -933,7 +941,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Bad Huffman Table index."); } - this.InputStream.Read(huffmanData.Array, 0, 16); + stream.Read(huffmanData.Array, 0, 16); using (IManagedByteBuffer codeLengths = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(17, AllocationOptions.Clean)) { @@ -954,7 +962,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using (IManagedByteBuffer huffmanValues = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean)) { - this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); + stream.Read(huffmanValues.Array, 0, codeLengthSum); i += 17 + codeLengthSum; @@ -973,32 +981,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in /// macroblocks /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessDefineRestartIntervalMarker(int remaining) + private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining) { if (remaining != 2) { JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining); } - this.resetInterval = this.ReadUint16(); + this.resetInterval = this.ReadUint16(stream); } /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScanMarker() + /// The input stream. + private void ProcessStartOfScanMarker(BufferedReadStream stream) { if (this.Frame is null) { JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); } - int selectorsCount = this.InputStream.ReadByte(); + int selectorsCount = stream.ReadByte(); for (int i = 0; i < selectorsCount; i++) { int componentIndex = -1; - int selector = this.InputStream.ReadByte(); + int selector = stream.ReadByte(); for (int j = 0; j < this.Frame.ComponentIds.Length; j++) { @@ -1016,20 +1026,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } ref JpegComponent component = ref this.Frame.Components[componentIndex]; - int tableSpec = this.InputStream.ReadByte(); + int tableSpec = stream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; this.Frame.ComponentOrder[i] = (byte)componentIndex; } - this.InputStream.Read(this.temp, 0, 3); + stream.Read(this.temp, 0, 3); int spectralStart = this.temp[0]; int spectralEnd = this.temp[1]; int successiveApproximation = this.temp[2]; var sd = new HuffmanScanDecoder( - this.InputStream, + stream, this.Frame, this.dcHuffmanTables, this.acHuffmanTables, @@ -1057,11 +1067,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Reads a from the stream advancing it by two bytes /// + /// The input stream. /// The [MethodImpl(InliningOptions.ShortMethod)] - private ushort ReadUint16() + private ushort ReadUint16(BufferedReadStream stream) { - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); } diff --git a/src/ImageSharp/Formats/Png/ImageExtensions.cs b/src/ImageSharp/Formats/Png/ImageExtensions.cs index 9188e43adf..e6a5265b27 100644 --- a/src/ImageSharp/Formats/Png/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Png/ImageExtensions.cs @@ -1,8 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png; @@ -13,6 +13,48 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + /// /// Saves the image to the given stream with the png format. /// @@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp /// /// The image this method extends. /// The stream to save the image to. - /// The options for the encoder. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream) => SaveAsPngAsync(source, stream, null); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. /// Thrown if the stream is null. public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index a6a040789a..87e0195c35 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(configuration, 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(configuration, 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(configuration, 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(configuration, 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..89caac3f68 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -9,7 +9,6 @@ using System.IO.Compression; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; @@ -44,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The stream to decode from. /// - private Stream currentStream; + private BufferedReadStream currentStream; /// /// The png header. @@ -132,7 +131,7 @@ namespace SixLabors.ImageSharp.Formats.Png public Size Dimensions => new Size(this.header.Width, this.header.Height); /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { var metadata = new ImageMetadata(); @@ -224,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); @@ -499,7 +498,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The compressed pixel data stream. /// The image to decode to. /// The png metadata - private void DecodePixelData(Stream compressedStream, ImageFrame image, PngMetadata pngMetadata) + private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { while (this.currentRow < this.header.Height) @@ -555,7 +554,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The compressed pixel data stream. /// The current image. /// The png metadata. - private void DecodeInterlacedPixelData(Stream compressedStream, ImageFrame image, PngMetadata pngMetadata) + private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { int pass = 0; @@ -1027,7 +1026,8 @@ namespace SixLabors.ImageSharp.Formats.Png private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) { using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var inflateStream = new ZlibInflateStream(memoryStream)) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) { if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs index 07316d37b7..52ef0e85ba 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.IO.Compression; +using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -27,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The inner raw memory stream. /// - private readonly Stream innerStream; + private readonly BufferedReadStream innerStream; /// /// A value indicating whether this instance of the given entity has been disposed. @@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// Initializes a new instance of the class. /// /// The inner raw stream. - public ZlibInflateStream(Stream innerStream) + public ZlibInflateStream(BufferedReadStream innerStream) : this(innerStream, GetDataNoOp) { } @@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The inner raw stream. /// A delegate to get more data from the inner stream. - public ZlibInflateStream(Stream innerStream, Func getData) + public ZlibInflateStream(BufferedReadStream innerStream, Func getData) { this.innerStream = innerStream; this.getData = getData; @@ -272,7 +273,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.currentDataRemaining -= 4; } - // Initialize the deflate Stream. + // Initialize the deflate BufferedReadStream. this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); return true; diff --git a/src/ImageSharp/Formats/Tga/ImageExtensions.cs b/src/ImageSharp/Formats/Tga/ImageExtensions.cs index 50e6c166ab..f39738eae3 100644 --- a/src/ImageSharp/Formats/Tga/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Tga/ImageExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Tga; @@ -13,6 +13,48 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null); + + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null); + + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + /// /// Saves the image to the given stream with the tga format. /// @@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp /// /// The image this method extends. /// The stream to save the image to. - /// The options for the encoder. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream) => SaveAsTgaAsync(source, stream, null); + + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. /// Thrown if the stream is null. public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 06b9ab6050..25aa233db8 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector { /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -23,21 +24,26 @@ namespace SixLabors.ImageSharp.Formats.Tga try { - return await decoder.DecodeAsync(stream).ConfigureAwait(false); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) { Size dims = decoder.Dimensions; - TgaThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); // Not reachable, as the previous statement will throw a exception. return null; } } + /// + public Image Decode(Configuration configuration, Stream stream) + => this.Decode(configuration, stream); + /// - public Image Decode(Configuration configuration, Stream stream) + public async Task> DecodeAsync(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -46,13 +52,14 @@ namespace SixLabors.ImageSharp.Formats.Tga try { - return decoder.Decode(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) { Size dims = decoder.Dimensions; - TgaThrowHelper.ThrowInvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); // Not reachable, as the previous statement will throw a exception. return null; @@ -60,17 +67,16 @@ namespace SixLabors.ImageSharp.Formats.Tga } /// - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream) + => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - return new TgaDecoderCore(configuration, this).Identify(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return new TgaDecoderCore(configuration, this).Identify(bufferedStream); } /// @@ -78,7 +84,8 @@ namespace SixLabors.ImageSharp.Formats.Tga { Guard.NotNull(stream, nameof(stream)); - return new TgaDecoderCore(configuration, this).IdentifyAsync(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); + return new TgaDecoderCore(configuration, this).IdentifyAsync(bufferedStream); } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 3f6d721f6a..7cd83fedbf 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -45,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// The stream to decode from. /// - private Stream currentStream; + private BufferedReadStream currentStream; /// /// The bitmap decoder options. @@ -78,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height); /// - public Image Decode(Stream stream) + public Image Decode(BufferedReadStream stream) where TPixel : unmanaged, IPixel { try @@ -641,7 +640,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } /// - public IImageInfo Identify(Stream stream) + public IImageInfo Identify(BufferedReadStream stream) { this.ReadFileHeader(stream); return new ImageInfo( @@ -868,9 +867,9 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Reads the tga file header from the stream. /// - /// The containing image data. + /// The containing image data. /// The image origin. - private TgaImageOrigin ReadFileHeader(Stream stream) + private TgaImageOrigin ReadFileHeader(BufferedReadStream stream) { this.currentStream = stream; diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs new file mode 100644 index 0000000000..02015eb56a --- /dev/null +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -0,0 +1,423 @@ +// 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 + { + private readonly int maxBufferIndex; + + 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 configuration which allows altering default behaviour or extending the library. + /// The input stream. + public BufferedReadStream(Configuration configuration, Stream stream) + { + Guard.NotNull(configuration, nameof(configuration)); + 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.Length = stream.Length; + this.Position = (int)stream.Position; + this.BufferSize = configuration.StreamProcessingBufferSize; + this.maxBufferIndex = this.BufferSize - 1; + this.readBuffer = ArrayPool.Shared.Rent(this.BufferSize); + this.readBufferHandle = new Memory(this.readBuffer).Pin(); + unsafe + { + this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; + } + + // This triggers a full read on first attempt. + this.readBufferIndex = this.BufferSize; + } + + /// + /// Gets the size, in bytes, of the underlying buffer. + /// + public int BufferSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + public override long Length { get; } + + /// + public override long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.readerPosition; + + [MethodImpl(MethodImplOptions.NoInlining)] + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position)); + Guard.MustBeLessThan(value, this.Length, nameof(this.Position)); + + // 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 = this.BufferSize; + } + } + } + + /// + 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 > this.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 > this.BufferSize) + { + 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 > this.BufferSize) + { + 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 > this.BufferSize) + { + 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 > this.BufferSize) + { + 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 = this.BufferSize; + } + + /// + [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 < this.BufferSize; + } + + [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, this.BufferSize - n); + n += i; + } + while (n < this.BufferSize && i > 0); + + this.readBufferIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(Span buffer) + { + int n = this.GetCopyCount(buffer.Length); + + // Just straight copy. MemoryStream does the same so should be fast enough. + this.readBuffer.AsSpan(this.readBufferIndex, n).CopyTo(buffer); + + this.readerPosition += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) + { + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.readerPosition += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopySlow(Span buffer) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(Span buffer) + { + // Read to target but don't copy to our read buffer. + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int count = buffer.Length; + int n = 0; + int i; + do + { + i = baseStream.Read(buffer.Slice(n, count - n)); + n += i; + } + while (n < count && i > 0); + + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our read buffer. + Stream baseStream = this.BaseStream; + if (this.readerPosition != baseStream.Position) + { + baseStream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = baseStream.Read(buffer, n + offset, count - n); + n += i; + } + while (n < count && i > 0); + + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCopyCount(int count) + { + long n = this.Length - this.readerPosition; + if (n > count) + { + return count; + } + + if (n < 0) + { + return 0; + } + + return (int)n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void CopyBytes(byte[] buffer, int offset, int count) + { + // Same as MemoryStream. + if (count < 9) + { + int byteCount = count; + int read = this.readBufferIndex; + byte* pinned = this.pinnedReadBuffer; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = pinned[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); + } + } + } +} diff --git a/src/ImageSharp/IO/DoubleBufferedStreamReader.cs b/src/ImageSharp/IO/DoubleBufferedStreamReader.cs deleted file mode 100644 index 079657c832..0000000000 --- a/src/ImageSharp/IO/DoubleBufferedStreamReader.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.IO; -using System.Runtime.CompilerServices; - -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.IO -{ - /// - /// A stream reader that add a secondary level buffer in addition to native stream buffered reading - /// to reduce the overhead of small incremental reads. - /// - internal sealed unsafe class DoubleBufferedStreamReader : IDisposable - { - /// - /// The length, in bytes, of the buffering chunk. - /// - public const int ChunkLength = 8192; - - private const int MaxChunkIndex = ChunkLength - 1; - - private readonly Stream stream; - - private readonly IManagedByteBuffer managedBuffer; - - private MemoryHandle handle; - - private readonly byte* pinnedChunk; - - private readonly byte[] bufferChunk; - - private readonly int length; - - private int chunkIndex; - - private int position; - - /// - /// Initializes a new instance of the class. - /// - /// The to use for buffer allocations. - /// The input stream. - public DoubleBufferedStreamReader(MemoryAllocator memoryAllocator, Stream stream) - { - this.stream = stream; - this.Position = (int)stream.Position; - this.length = (int)stream.Length; - this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(ChunkLength); - this.bufferChunk = this.managedBuffer.Array; - this.handle = this.managedBuffer.Memory.Pin(); - this.pinnedChunk = (byte*)this.handle.Pointer; - this.chunkIndex = ChunkLength; - } - - /// - /// Gets the length, in bytes, of the stream. - /// - public long Length => this.length; - - /// - /// Gets or sets the current position within the stream. - /// - public long Position - { - get => this.position; - - set - { - // Only reset chunkIndex if we are out of bounds of our working chunk - // otherwise we should simply move the value by the diff. - int v = (int)value; - if (this.IsInChunk(v, out int index)) - { - this.chunkIndex = index; - this.position = v; - } - else - { - this.position = v; - this.stream.Seek(value, SeekOrigin.Begin); - this.chunkIndex = ChunkLength; - } - } - } - - /// - /// Reads a byte from the stream and advances the position within the stream by one - /// byte, or returns -1 if at the end of the stream. - /// - /// The unsigned byte cast to an , or -1 if at the end of the stream. - [MethodImpl(InliningOptions.ShortMethod)] - public int ReadByte() - { - if (this.position >= this.length) - { - return -1; - } - - if (this.chunkIndex > MaxChunkIndex) - { - this.FillChunk(); - } - - this.position++; - return this.pinnedChunk[this.chunkIndex++]; - } - - /// - /// Skips the number of bytes in the stream - /// - /// The number of bytes to skip. - [MethodImpl(InliningOptions.ShortMethod)] - public void Skip(int count) => this.Position += count; - - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream - /// by the number of bytes read. - /// - /// - /// An array of bytes. When this method returns, the buffer contains the specified - /// byte array with the values between offset and (offset + count - 1) replaced by - /// the bytes read from the current source. - /// - /// - /// The zero-based byte offset in buffer at which to begin storing the data read - /// from the current stream. - /// - /// The maximum number of bytes to be read from the current stream. - /// - /// The total number of bytes read into the buffer. This can be less than the number - /// of bytes requested if that many bytes are not currently available, or zero (0) - /// if the end of the stream has been reached. - /// - [MethodImpl(InliningOptions.ShortMethod)] - public int Read(byte[] buffer, int offset, int count) - { - if (count > ChunkLength) - { - return this.ReadToBufferSlow(buffer, offset, count); - } - - if (count + this.chunkIndex > ChunkLength) - { - return this.ReadToChunkSlow(buffer, offset, count); - } - - int n = this.GetCopyCount(count); - this.CopyBytes(buffer, offset, n); - - this.position += n; - this.chunkIndex += n; - return n; - } - - /// - public void Dispose() - { - this.handle.Dispose(); - this.managedBuffer?.Dispose(); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private int GetPositionDifference(int p) => p - this.position; - - [MethodImpl(InliningOptions.ShortMethod)] - private bool IsInChunk(int p, out int index) - { - index = this.GetPositionDifference(p) + this.chunkIndex; - return index > -1 && index < ChunkLength; - } - - [MethodImpl(InliningOptions.ColdPath)] - private void FillChunk() - { - if (this.position != this.stream.Position) - { - this.stream.Seek(this.position, SeekOrigin.Begin); - } - - this.stream.Read(this.bufferChunk, 0, ChunkLength); - this.chunkIndex = 0; - } - - [MethodImpl(InliningOptions.ColdPath)] - private int ReadToChunkSlow(byte[] buffer, int offset, int count) - { - // Refill our buffer then copy. - this.FillChunk(); - - int n = this.GetCopyCount(count); - this.CopyBytes(buffer, offset, n); - - this.position += n; - this.chunkIndex += n; - - return n; - } - - [MethodImpl(InliningOptions.ColdPath)] - private int ReadToBufferSlow(byte[] buffer, int offset, int count) - { - // Read to target but don't copy to our chunk. - if (this.position != this.stream.Position) - { - this.stream.Seek(this.position, SeekOrigin.Begin); - } - - int n = this.stream.Read(buffer, offset, count); - this.Position += n; - - return n; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private int GetCopyCount(int count) - { - int n = this.length - this.position; - if (n > count) - { - n = count; - } - - if (n < 0) - { - n = 0; - } - - return n; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private void CopyBytes(byte[] buffer, int offset, int count) - { - if (count < 9) - { - int byteCount = count; - int read = this.chunkIndex; - byte* pinned = this.pinnedChunk; - - while (--byteCount > -1) - { - buffer[offset + byteCount] = pinned[read + byteCount]; - } - } - else - { - Buffer.BlockCopy(this.bufferChunk, this.chunkIndex, buffer, offset, count); - } - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 5330782f22..683590fd1a 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -59,7 +59,18 @@ namespace SixLabors.ImageSharp using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(headerSize, AllocationOptions.Clean)) { long startPosition = stream.Position; - stream.Read(buffer.Array, 0, headerSize); + + // Read doesn't always guarantee the full returned length so read a byte + // at a time until we get either our count or hit the end of the stream. + int n = 0; + int i; + do + { + i = stream.Read(buffer.Array, n, headerSize - n); + n += i; + } + while (n < headerSize && i > 0); + stream.Position = startPosition; // Does the given stream contain enough data to fit in the header for the format diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index d3fd35d5fc..fae88f21e1 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -7,7 +7,6 @@ using System.IO; using System.Text; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -655,7 +654,18 @@ namespace SixLabors.ImageSharp throw new UnknownImageFormatException(sb.ToString()); } - private static T WithSeekableStream(Configuration configuration, Stream stream, Func action) + /// + /// Performs the given action against the stream ensuring that it is seekable. + /// + /// The type of object returned from the action. + /// The configuration. + /// The input stream. + /// The action to perform. + /// The . + private static T WithSeekableStream( + Configuration configuration, + Stream stream, + Func action) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(stream, nameof(stream)); @@ -676,15 +686,21 @@ namespace SixLabors.ImageSharp } // We want to be able to load images from things like HttpContext.Request.Body - using (MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length)) - { - stream.CopyTo(memoryStream); - memoryStream.Position = 0; + using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); + stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); + memoryStream.Position = 0; - return action(memoryStream); - } + return action(memoryStream); } + /// + /// Performs the given action asynchronously against the stream ensuring that it is seekable. + /// + /// The type of object returned from the action. + /// The configuration. + /// The input stream. + /// The action to perform. + /// The . private static async Task WithSeekableStreamAsync( Configuration configuration, Stream stream, @@ -712,13 +728,11 @@ namespace SixLabors.ImageSharp return await action(stream).ConfigureAwait(false); } - using (MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length)) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize).ConfigureAwait(false); + memoryStream.Position = 0; - return await action(memoryStream).ConfigureAwait(false); - } + return await action(memoryStream).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 7800a5c6f2..605f5e0da8 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -103,15 +103,15 @@ namespace SixLabors.ImageSharp /// /// The stream to save the image to. /// The encoder to save the image with. - /// Thrown if the stream or encoder is null. + /// Thrown if the stream or encoder is null. /// A representing the asynchronous operation. - public async Task SaveAsync(Stream stream, IImageEncoder encoder) + public Task SaveAsync(Stream stream, IImageEncoder encoder) { Guard.NotNull(stream, nameof(stream)); Guard.NotNull(encoder, nameof(encoder)); this.EnsureNotDisposed(); - await this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)).ConfigureAwait(false); + return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)); } /// @@ -179,9 +179,8 @@ namespace SixLabors.ImageSharp public void Visit(Image image) where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream); - public async Task VisitAsync(Image image) - where TPixel : unmanaged, IPixel - => await this.encoder.EncodeAsync(image, this.stream).ConfigureAwait(false); + public Task VisitAsync(Image image) + where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream); } } } diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index b2d2b1c319..e9e93a855f 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -25,14 +25,14 @@ - - - + + + - + diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index dfedf3d89b..8c597a8c5b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -6,6 +6,7 @@ using System.IO; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Tests; using SDSize = System.Drawing.Size; @@ -30,24 +31,20 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg [Benchmark(Baseline = true, Description = "System.Drawing FULL")] public SDSize JpegSystemDrawing() { - using (var memoryStream = new MemoryStream(this.jpegBytes)) - { - using (var image = System.Drawing.Image.FromStream(memoryStream)) - { - return image.Size; - } - } + using var memoryStream = new MemoryStream(this.jpegBytes); + using var image = System.Drawing.Image.FromStream(memoryStream); + return image.Size; } [Benchmark(Description = "JpegDecoderCore.ParseStream")] public void ParseStreamPdfJs() { - using (var memoryStream = new MemoryStream(this.jpegBytes)) - { - var decoder = new JpegDecoderCore(Configuration.Default, new Formats.Jpeg.JpegDecoder { IgnoreMetadata = true }); - decoder.ParseStream(memoryStream); - decoder.Dispose(); - } + using var memoryStream = new MemoryStream(this.jpegBytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream); + + var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true }); + decoder.ParseStream(bufferedStream); + decoder.Dispose(); } // RESULTS (2019 April 23): diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs index 2ac02c7d53..620a4d5edf 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg public ShortClr() { // Job.Default.With(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3), - this.Add(Job.Default.With(CoreRuntime.Core21).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3)); + this.Add(Job.Default.With(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3)); } } } @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); - #pragma warning disable SA1115 +#pragma warning disable SA1115 [Params( TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr, diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs deleted file mode 100644 index 77719673f5..0000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortClr))] - public class DoubleBufferedStreams - { - private readonly byte[] buffer = CreateTestBytes(); - private readonly byte[] chunk1 = new byte[2]; - private readonly byte[] chunk2 = new byte[2]; - - private MemoryStream stream1; - private MemoryStream stream2; - private MemoryStream stream3; - private MemoryStream stream4; - private DoubleBufferedStreamReader reader1; - private DoubleBufferedStreamReader reader2; - - [GlobalSetup] - public void CreateStreams() - { - this.stream1 = new MemoryStream(this.buffer); - this.stream2 = new MemoryStream(this.buffer); - this.stream3 = new MemoryStream(this.buffer); - this.stream4 = new MemoryStream(this.buffer); - this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); - this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); - } - - [GlobalCleanup] - public void DestroyStreams() - { - this.stream1?.Dispose(); - this.stream2?.Dispose(); - this.stream3?.Dispose(); - this.stream4?.Dispose(); - this.reader1?.Dispose(); - this.reader2?.Dispose(); - } - - [Benchmark(Baseline = true)] - public int StandardStreamReadByte() - { - int r = 0; - Stream stream = this.stream1; - - for (int i = 0; i < stream.Length; i++) - { - r += stream.ReadByte(); - } - - return r; - } - - [Benchmark] - public int StandardStreamRead() - { - int r = 0; - Stream stream = this.stream1; - byte[] b = this.chunk1; - - for (int i = 0; i < stream.Length / 2; i++) - { - r += stream.Read(b, 0, 2); - } - - return r; - } - - [Benchmark] - public int DoubleBufferedStreamReadByte() - { - int r = 0; - DoubleBufferedStreamReader reader = this.reader1; - - for (int i = 0; i < reader.Length; i++) - { - r += reader.ReadByte(); - } - - return r; - } - - [Benchmark] - public int DoubleBufferedStreamRead() - { - int r = 0; - DoubleBufferedStreamReader reader = this.reader2; - byte[] b = this.chunk2; - - for (int i = 0; i < reader.Length / 2; i++) - { - r += reader.Read(b, 0, 2); - } - - return r; - } - - [Benchmark] - public int SimpleReadByte() - { - byte[] b = this.buffer; - int r = 0; - for (int i = 0; i < b.Length; i++) - { - r += b[i]; - } - - return r; - } - - private static byte[] CreateTestBytes() - { - var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; - var random = new Random(); - random.NextBytes(buffer); - - return buffer; - } - } - - /* RESULTS (2019 April 24): - - BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17763.437 (1809/October2018Update/Redstone5) - Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 2 physical cores - .NET Core SDK=2.2.202 - [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT - Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0 - Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT - - IterationCount=3 LaunchCount=1 WarmupCount=3 - - | Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------- |----- |-------- |---------:|-----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | StandardStreamReadByte | Clr | Clr | 96.71 us | 5.9950 us | 0.3286 us | 1.00 | 0.00 | - | - | - | - | - | StandardStreamRead | Clr | Clr | 77.73 us | 5.2284 us | 0.2866 us | 0.80 | 0.00 | - | - | - | - | - | DoubleBufferedStreamReadByte | Clr | Clr | 23.17 us | 26.2354 us | 1.4381 us | 0.24 | 0.01 | - | - | - | - | - | DoubleBufferedStreamRead | Clr | Clr | 33.35 us | 3.4071 us | 0.1868 us | 0.34 | 0.00 | - | - | - | - | - | SimpleReadByte | Clr | Clr | 10.85 us | 0.4927 us | 0.0270 us | 0.11 | 0.00 | - | - | - | - | - | | | | | | | | | | | | | - | StandardStreamReadByte | Core | Core | 75.35 us | 12.9789 us | 0.7114 us | 1.00 | 0.00 | - | - | - | - | - | StandardStreamRead | Core | Core | 55.36 us | 1.4432 us | 0.0791 us | 0.73 | 0.01 | - | - | - | - | - | DoubleBufferedStreamReadByte | Core | Core | 21.47 us | 29.7076 us | 1.6284 us | 0.28 | 0.02 | - | - | - | - | - | DoubleBufferedStreamRead | Core | Core | 29.67 us | 2.5988 us | 0.1424 us | 0.39 | 0.00 | - | - | - | - | - | SimpleReadByte | Core | Core | 10.84 us | 0.7567 us | 0.0415 us | 0.14 | 0.00 | - | - | - | - | - */ -} diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs new file mode 100644 index 0000000000..baabb4784b --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Benchmarks.IO +{ + /// + /// A readonly stream wrapper that add a secondary level buffer in addition to native stream + /// buffered reading to reduce the overhead of small incremental reads. + /// + internal sealed unsafe class BufferedReadStreamWrapper : IDisposable + { + /// + /// The length, in bytes, of the underlying buffer. + /// + public const int BufferLength = 8192; + + private const int MaxBufferIndex = BufferLength - 1; + + private readonly Stream stream; + + private readonly byte[] readBuffer; + + private MemoryHandle readBufferHandle; + + private readonly byte* pinnedReadBuffer; + + // Index within our buffer, not reader position. + private int readBufferIndex; + + // Matches what the stream position would be without buffering + private long readerPosition; + + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + public BufferedReadStreamWrapper(Stream stream) + { + Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); + Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); + + // Ensure all underlying buffers have been flushed before we attempt to read the stream. + // User streams may have opted to throw from Flush if CanWrite is false + // (although the abstract Stream does not do so). + if (stream.CanWrite) + { + stream.Flush(); + } + + this.stream = stream; + this.Position = (int)stream.Position; + this.Length = stream.Length; + + this.readBuffer = ArrayPool.Shared.Rent(BufferLength); + this.readBufferHandle = new Memory(this.readBuffer).Pin(); + this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; + + // This triggers a full read on first attempt. + this.readBufferIndex = BufferLength; + } + + /// + /// Gets the length, in bytes, of the stream. + /// + public long Length { get; } + + /// + /// Gets or sets the current position within the stream. + /// + public long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.readerPosition; + + [MethodImpl(MethodImplOptions.NoInlining)] + set + { + // Only reset readBufferIndex if we are out of bounds of our working buffer + // otherwise we should simply move the value by the diff. + if (this.IsInReadBuffer(value, out long index)) + { + this.readBufferIndex = (int)index; + this.readerPosition = value; + } + else + { + // Base stream seek will throw for us if invalid. + this.stream.Seek(value, SeekOrigin.Begin); + this.readerPosition = value; + this.readBufferIndex = BufferLength; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadByte() + { + if (this.readerPosition >= this.Length) + { + return -1; + } + + // Our buffer has been read. + // We need to refill and start again. + if (this.readBufferIndex > MaxBufferIndex) + { + this.FillReadBuffer(); + } + + this.readerPosition++; + return this.pinnedReadBuffer[this.readBufferIndex++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read(byte[] buffer, int offset, int count) + { + // Too big for our buffer. Read directly from the stream. + if (count > BufferLength) + { + return this.ReadToBufferDirectSlow(buffer, offset, count); + } + + // Too big for remaining buffer but less than entire buffer length + // Copy to buffer then read from there. + if (count + this.readBufferIndex > BufferLength) + { + return this.ReadToBufferViaCopySlow(buffer, offset, count); + } + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + public void Flush() + { + // Reset the stream position to match reader position. + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + this.readerPosition = (int)this.stream.Position; + } + + // Reset to trigger full read on next attempt. + this.readBufferIndex = BufferLength; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + this.Position = offset; + break; + + case SeekOrigin.Current: + this.Position += offset; + break; + + case SeekOrigin.End: + this.Position = this.Length - offset; + break; + } + + return this.readerPosition; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.readBufferHandle.Dispose(); + ArrayPool.Shared.Return(this.readBuffer); + this.Flush(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsInReadBuffer(long newPosition, out long index) + { + index = newPosition - this.readerPosition + this.readBufferIndex; + return index > -1 && index < BufferLength; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FillReadBuffer() + { + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + this.stream.Read(this.readBuffer, 0, BufferLength); + this.readBufferIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) + { + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.readerPosition += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our read buffer. + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + int n = this.stream.Read(buffer, offset, count); + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCopyCount(int count) + { + long n = this.Length - this.readerPosition; + if (n > count) + { + return count; + } + + if (n < 0) + { + return 0; + } + + return (int)n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyBytes(byte[] buffer, int offset, int count) + { + // Same as MemoryStream. + if (count < 9) + { + int byteCount = count; + int read = this.readBufferIndex; + byte* pinned = this.pinnedReadBuffer; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = pinned[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs new file mode 100644 index 0000000000..be232c78d6 --- /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(Configuration.Default, this.stream3); + this.bufferedStream2 = new BufferedReadStream(Configuration.Default, 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[Configuration.Default.StreamProcessingBufferSize * 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/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index d53458affd..f6111da5a3 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -76,10 +76,7 @@ namespace SixLabors.ImageSharp.Tests if (throws) { Assert.Throws( - () => - { - cfg.MaxDegreeOfParallelism = maxDegreeOfParallelism; - }); + () => cfg.MaxDegreeOfParallelism = maxDegreeOfParallelism); } else { @@ -122,7 +119,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void DefaultConfigurationHasCorrectFormatCount() { - Configuration config = Configuration.CreateDefaultInstance(); + var config = Configuration.CreateDefaultInstance(); Assert.Equal(this.expectedDefaultConfigurationCount, config.ImageFormats.Count()); } @@ -133,5 +130,21 @@ namespace SixLabors.ImageSharp.Tests Configuration config = this.DefaultConfiguration; Assert.True(config.WorkingBufferSizeHintInBytes > 1024); } + + [Fact] + public void StreamBufferSize_DefaultIsCorrect() + { + Configuration config = this.DefaultConfiguration; + Assert.True(config.StreamProcessingBufferSize == 8096); + } + + [Fact] + public void StreamBufferSize_CannotGoBelowMinimum() + { + var config = new Configuration(); + + Assert.Throws( + () => config.StreamProcessingBufferSize = 0); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Bmp/ImageExtensionsTest.cs new file mode 100644 index 0000000000..5428ddbdca --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Bmp/ImageExtensionsTest.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Bmp +{ + public class ImageExtensionsTest + { + [Fact] + public void SaveAsBmp_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsBmp_Path.bmp"); + + using (var image = new Image(10, 10)) + { + image.SaveAsBmp(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsBmpAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsBmpAsync_Path.bmp"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsBmpAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsBmp_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsBmp_Path_Encoder.bmp"); + + using (var image = new Image(10, 10)) + { + image.SaveAsBmp(file, new BmpEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsBmpAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsBmpAsync_Path_Encoder.bmp"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsBmpAsync(file, new BmpEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsBmp_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsBmp(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsBmpAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsBmpAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsBmp_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsBmp(memoryStream, new BmpEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsBmpAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsBmpAsync(memoryStream, new BmpEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/bmp", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Gif/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Gif/ImageExtensionsTest.cs new file mode 100644 index 0000000000..50b0fc6bf8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Gif/ImageExtensionsTest.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Gif +{ + public class ImageExtensionsTest + { + [Fact] + public void SaveAsGif_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsGif_Path.gif"); + + using (var image = new Image(10, 10)) + { + image.SaveAsGif(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsGifAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsGifAsync_Path.gif"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsGifAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsGif_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsGif_Path_Encoder.gif"); + + using (var image = new Image(10, 10)) + { + image.SaveAsGif(file, new GifEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsGifAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsGifAsync_Path_Encoder.gif"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsGifAsync(file, new GifEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsGif_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsGif(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsGifAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsGifAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsGif_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsGif(memoryStream, new GifEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsGifAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsGifAsync(memoryStream, new GifEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/gif", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Jpg/ImageExtensionsTest.cs new file mode 100644 index 0000000000..9b67bcd1eb --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/ImageExtensionsTest.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + public class ImageExtensionsTest + { + [Fact] + public void SaveAsJpeg_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsJpeg_Path.jpg"); + + using (var image = new Image(10, 10)) + { + image.SaveAsJpeg(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsJpegAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsJpegAsync_Path.jpg"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsJpegAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsJpeg_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsJpeg_Path_Encoder.jpg"); + + using (var image = new Image(10, 10)) + { + image.SaveAsJpeg(file, new JpegEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsJpegAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsJpegAsync_Path_Encoder.jpg"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsJpegAsync(file, new JpegEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsJpeg_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsJpeg(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsJpegAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsJpegAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsJpeg_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsJpeg(memoryStream, new JpegEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsJpegAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsJpegAsync(memoryStream, new JpegEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/jpeg", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index e69ba98f91..912f606b2c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; @@ -71,16 +72,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void ParseStream_BasicPropertiesAreCorrect() { byte[] bytes = TestFile.Create(TestImages.Jpeg.Progressive.Progress).Bytes; - using (var ms = new MemoryStream(bytes)) - { - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); - decoder.ParseStream(ms); - - // I don't know why these numbers are different. All I know is that the decoder works - // and spectral data is exactly correct also. - // VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31); - VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 44, 62, 22, 31, 22, 31); - } + using var ms = new MemoryStream(bytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, 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..662ea9e330 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; @@ -51,13 +52,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; - using (var ms = new MemoryStream(sourceBytes)) - { - decoder.ParseStream(ms); + using var ms = new MemoryStream(sourceBytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); + decoder.ParseStream(bufferedStream); - var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); - VerifyJpeg.SaveSpectralImage(provider, data); - } + var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); + VerifyJpeg.SaveSpectralImage(provider, data); } [Theory] @@ -74,13 +74,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; - using (var ms = new MemoryStream(sourceBytes)) - { - decoder.ParseStream(ms); - var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); + using var ms = new MemoryStream(sourceBytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, 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..c6f4704f05 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -8,7 +8,7 @@ using System.Text; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; - +using SixLabors.ImageSharp.IO; using Xunit; using Xunit.Abstractions; @@ -192,12 +192,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils internal static JpegDecoderCore ParseJpegStream(string testFileName, bool metaDataOnly = false) { byte[] bytes = TestFile.Create(testFileName).Bytes; - using (var ms = new MemoryStream(bytes)) - { - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); - decoder.ParseStream(ms, metaDataOnly); - return decoder; - } + using var ms = new MemoryStream(bytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); + + var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + decoder.ParseStream(bufferedStream, metaDataOnly); + + return decoder; } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Png/ImageExtensionsTest.cs new file mode 100644 index 0000000000..f0493f1767 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/ImageExtensionsTest.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Png +{ + public class ImageExtensionsTest + { + [Fact] + public void SaveAsPng_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsPng_Path.png"); + + using (var image = new Image(10, 10)) + { + image.SaveAsPng(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsPngAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsPngAsync_Path.png"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsPngAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsPng_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsPng_Path_Encoder.png"); + + using (var image = new Image(10, 10)) + { + image.SaveAsPng(file, new PngEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsPngAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsPngAsync_Path_Encoder.png"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsPngAsync(file, new PngEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsPng_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsPng(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsPngAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsPngAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsPng_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsPng(memoryStream, new PngEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsPngAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsPngAsync(memoryStream, new PngEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/png", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 6284191f39..1ec7e24486 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png var decoder = new PngDecoder(); ImageFormatException exception = - Assert.Throws(() => decoder.Decode(null, memStream)); + Assert.Throws(() => decoder.Decode(Configuration.Default, memStream)); Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message); } diff --git a/tests/ImageSharp.Tests/Formats/Tga/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Tga/ImageExtensionsTest.cs new file mode 100644 index 0000000000..3bde32b97f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tga/ImageExtensionsTest.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tga +{ + public class ImageExtensionsTest + { + [Fact] + public void SaveAsTga_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTga_Path.tga"); + + using (var image = new Image(10, 10)) + { + image.SaveAsTga(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTgaAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTgaAsync_Path.tga"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTgaAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsTga_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsTga_Path_Encoder.tga"); + + using (var image = new Image(10, 10)) + { + image.SaveAsTga(file, new TgaEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTgaAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsTgaAsync_Path_Encoder.tga"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTgaAsync(file, new TgaEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsTga_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsTga(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTgaAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTgaAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsTga_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsTga(memoryStream, new TgaEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTgaAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTgaAsync(memoryStream, new TgaEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tga", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs index af93884fda..0f76d99317 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -5,8 +5,6 @@ using System; using System.IO; using ImageMagick; - -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs new file mode 100644 index 0000000000..6ceaca012e --- /dev/null +++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs @@ -0,0 +1,354 @@ +// 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 + { + private readonly Configuration configuration; + + public BufferedReadStreamTests() + { + this.configuration = Configuration.CreateDefaultInstance(); + } + + public static readonly TheoryData BufferSizes = + new TheoryData() + { + 1, 2, 4, 8, + 16, 97, 503, + 719, 1024, + 8096, 64768 + }; + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSingleByteFromOrigin(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + Assert.Equal(expected[0], reader.ReadByte()); + + // We've read a whole chunk but increment by 1 in our reader. + Assert.True(stream.Position >= bufferSize); + Assert.Equal(1, reader.Position); + } + + // Position of the stream should be reset on disposal. + Assert.Equal(1, stream.Position); + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSingleByteFromOffset(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + byte[] expected = stream.ToArray(); + int offset = expected.Length / 2; + using (var reader = new BufferedReadStream(this.configuration, 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(bufferSize + offset, stream.Position); + Assert.Equal(offset + 1, reader.Position); + } + + Assert.Equal(offset + 1, stream.Position); + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentSingleByteCorrectly(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + byte[] expected = stream.ToArray(); + int i; + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + for (i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], reader.ReadByte()); + Assert.Equal(i + 1, reader.Position); + + if (i < bufferSize) + { + Assert.Equal(stream.Position, bufferSize); + } + else if (i >= bufferSize && i < bufferSize * 2) + { + // We should have advanced to the second chunk now. + Assert.Equal(stream.Position, bufferSize * 2); + } + else + { + // We should have advanced to the third chunk now. + Assert.Equal(stream.Position, bufferSize * 3); + } + } + } + + Assert.Equal(i, stream.Position); + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadMultipleBytesFromOrigin(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + var buffer = new byte[2]; + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, 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.True(stream.Position >= bufferSize); + Assert.Equal(buffer.Length, reader.Position); + } + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentMultipleByteCorrectly(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + const int increment = 2; + var buffer = new byte[2]; + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment) + { + // Check values are correct. + Assert.Equal(increment, reader.Read(buffer, 0, increment)); + Assert.Equal(expected[o], buffer[0]); + Assert.Equal(expected[o + 1], buffer[1]); + Assert.Equal(o + increment, reader.Position); + + // These tests ensure that we are correctly reading + // our buffer in chunks of the given size. + int offset = i * increment; + + // First chunk. + if (offset < bufferSize) + { + // We've read an entire chunk once and are + // now reading from that chunk. + Assert.True(stream.Position >= bufferSize); + continue; + } + + // Second chunk + if (offset < bufferSize * 2) + { + Assert.True(stream.Position > bufferSize); + + // Odd buffer size with even increments can + // jump to the third chunk on final read. + Assert.True(stream.Position <= bufferSize * 3); + continue; + } + + // Third chunk + Assert.True(stream.Position > bufferSize * 2); + } + } + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + const int increment = 2; + Span buffer = new byte[2]; + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment) + { + // Check values are correct. + Assert.Equal(increment, reader.Read(buffer, 0, increment)); + Assert.Equal(expected[o], buffer[0]); + Assert.Equal(expected[o + 1], buffer[1]); + Assert.Equal(o + increment, reader.Position); + + // These tests ensure that we are correctly reading + // our buffer in chunks of the given size. + int offset = i * increment; + + // First chunk. + if (offset < bufferSize) + { + // We've read an entire chunk once and are + // now reading from that chunk. + Assert.True(stream.Position >= bufferSize); + continue; + } + + // Second chunk + if (offset < bufferSize * 2) + { + Assert.True(stream.Position > bufferSize); + + // Odd buffer size with even increments can + // jump to the third chunk on final read. + Assert.True(stream.Position <= bufferSize * 3); + continue; + } + + // Third chunk + Assert.True(stream.Position > bufferSize * 2); + } + } + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSkip(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 4)) + { + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + int skip = 1; + int plusOne = 1; + int skip2 = bufferSize; + + // 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()); + } + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamReadsSmallStream(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + + // Create a stream smaller than the default buffer length + using (MemoryStream stream = this.CreateTestStream(Math.Max(1, bufferSize / 4))) + { + byte[] expected = stream.ToArray(); + int offset = expected.Length / 2; + using (var reader = new BufferedReadStream(this.configuration, 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(Math.Max(1, bufferSize / 4), stream.Position); + Assert.Equal(offset + 1, reader.Position); + } + + Assert.Equal(offset + 1, stream.Position); + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) + { + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], reader.ReadByte()); + } + } + } + } + + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamThrowsOnBadPosition(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize)) + { + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + Assert.Throws(() => reader.Position = -stream.Length); + Assert.Throws(() => reader.Position = stream.Length); + } + } + } + + private MemoryStream CreateTestStream(int length) + { + var buffer = new byte[length]; + var random = new Random(); + random.NextBytes(buffer); + + return new EvilStream(buffer); + } + + // Simulates a stream that can only return 1 byte at a time per read instruction. + // See https://github.com/SixLabors/ImageSharp/issues/1268 + private class EvilStream : MemoryStream + { + public EvilStream(byte[] buffer) + : base(buffer) + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return base.Read(buffer, offset, 1); + } + } + } +} diff --git a/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs b/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs deleted file mode 100644 index 73010fe2ca..0000000000 --- a/tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; -using Xunit; - -namespace SixLabors.ImageSharp.Tests.IO -{ - public class DoubleBufferedStreamReaderTests - { - private readonly MemoryAllocator allocator = Configuration.Default.MemoryAllocator; - - [Fact] - public void DoubleBufferedStreamReaderCanReadSingleByteFromOrigin() - { - using (MemoryStream stream = this.CreateTestStream()) - { - byte[] expected = stream.ToArray(); - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - - Assert.Equal(expected[0], reader.ReadByte()); - - // We've read a whole chunk but increment by 1 in our reader. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); - Assert.Equal(1, reader.Position); - } - } - - [Fact] - public void DoubleBufferedStreamReaderCanReadSingleByteFromOffset() - { - using (MemoryStream stream = this.CreateTestStream()) - { - byte[] expected = stream.ToArray(); - const int offset = 5; - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - reader.Position = offset; - - Assert.Equal(expected[offset], reader.ReadByte()); - - // We've read a whole chunk but increment by 1 in our reader. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength + offset); - Assert.Equal(offset + 1, reader.Position); - } - } - - [Fact] - public void DoubleBufferedStreamReaderCanReadSubsequentSingleByteCorrectly() - { - using (MemoryStream stream = this.CreateTestStream()) - { - byte[] expected = stream.ToArray(); - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i], reader.ReadByte()); - Assert.Equal(i + 1, reader.Position); - - if (i < DoubleBufferedStreamReader.ChunkLength) - { - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); - } - else if (i >= DoubleBufferedStreamReader.ChunkLength && i < DoubleBufferedStreamReader.ChunkLength * 2) - { - // We should have advanced to the second chunk now. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); - } - else - { - // We should have advanced to the third chunk now. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); - } - } - } - } - - [Fact] - public void DoubleBufferedStreamReaderCanReadMultipleBytesFromOrigin() - { - using (MemoryStream stream = this.CreateTestStream()) - { - var buffer = new byte[2]; - byte[] expected = stream.ToArray(); - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - - Assert.Equal(2, reader.Read(buffer, 0, 2)); - Assert.Equal(expected[0], buffer[0]); - Assert.Equal(expected[1], buffer[1]); - - // We've read a whole chunk but increment by the buffer length in our reader. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); - Assert.Equal(buffer.Length, reader.Position); - } - } - - [Fact] - public void DoubleBufferedStreamReaderCanReadSubsequentMultipleByteCorrectly() - { - using (MemoryStream stream = this.CreateTestStream()) - { - var buffer = new byte[2]; - byte[] expected = stream.ToArray(); - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - - for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) - { - Assert.Equal(2, reader.Read(buffer, 0, 2)); - Assert.Equal(expected[o], buffer[0]); - Assert.Equal(expected[o + 1], buffer[1]); - Assert.Equal(o + 2, reader.Position); - - int offset = i * 2; - if (offset < DoubleBufferedStreamReader.ChunkLength) - { - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); - } - else if (offset >= DoubleBufferedStreamReader.ChunkLength && offset < DoubleBufferedStreamReader.ChunkLength * 2) - { - // We should have advanced to the second chunk now. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); - } - else - { - // We should have advanced to the third chunk now. - Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); - } - } - } - } - - [Fact] - public void DoubleBufferedStreamReaderCanSkip() - { - using (MemoryStream stream = this.CreateTestStream()) - { - byte[] expected = stream.ToArray(); - var reader = new DoubleBufferedStreamReader(this.allocator, stream); - - int skip = 50; - int plusOne = 1; - int skip2 = DoubleBufferedStreamReader.ChunkLength; - - // Skip - reader.Skip(skip); - Assert.Equal(skip, reader.Position); - Assert.Equal(stream.Position, reader.Position); - - // Read - Assert.Equal(expected[skip], reader.ReadByte()); - - // Skip Again - reader.Skip(skip2); - - // First Skip + First Read + Second Skip - int position = skip + plusOne + skip2; - - Assert.Equal(position, reader.Position); - Assert.Equal(stream.Position, reader.Position); - Assert.Equal(expected[position], reader.ReadByte()); - } - } - - private MemoryStream CreateTestStream() - { - var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; - var random = new Random(); - random.NextBytes(buffer); - - return new MemoryStream(buffer); - } - } -} diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs index 7478f76f08..9d4ffdace7 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs @@ -81,9 +81,9 @@ namespace SixLabors.ImageSharp.Tests { Assert.Throws( () => - { - Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString()); - }); + { + Image.Load(this.TopLevelConfiguration, Guid.NewGuid().ToString()); + }); } [Fact] @@ -91,9 +91,9 @@ namespace SixLabors.ImageSharp.Tests { Assert.Throws( () => - { - Image.Load(this.TopLevelConfiguration, (string)null); - }); + { + Image.Load(this.TopLevelConfiguration, (string)null); + }); } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 4d1e754402..fae3ff5a5b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -3,12 +3,9 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; using ImageMagick; - -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats;