diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 346bfd534..89d1a75f2 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,11 +12,11 @@ * 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. @@ -33,14 +33,12 @@ #### **Do you have questions about consuming the library or the source code?** -* Ask any question about how to use ImageSharp over in the [discussions section](https://github.com/SixLabors/ImageSharp/discussions). +* 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 5a9d1dde0..cf9f78752 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/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 067257132..062bcb229 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; @@ -26,7 +25,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; /// @@ -75,6 +75,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. /// @@ -145,6 +163,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 7e8ac0721..cb26ff606 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return new BmpDecoderCore(configuration, this).Identify(bufferedStream); } @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return new BmpDecoderCore(configuration, this).IdentifyAsync(bufferedStream); } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 2a5fde6ac..2b7103072 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Gif try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Gif try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Gif var decoder = new GifDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Identify(bufferedStream); } @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Gif var decoder = new GifDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.IdentifyAsync(bufferedStream); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index c5332acb5..3eaf3a4c4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var decoder = new JpegDecoderCore(configuration, this); try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var decoder = new JpegDecoderCore(configuration, this); try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Identify(bufferedStream); } @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + 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 2956d2c11..c0b09c4c2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -514,6 +514,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // TODO: thumbnail if (remaining > 0) { + if (stream.Position + remaining >= stream.Length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad App0 Marker length."); + } + stream.Skip(remaining); } } @@ -533,6 +538,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return; } + if (stream.Position + remaining >= stream.Length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); + } + var profile = new byte[remaining]; stream.Read(profile, 0, remaining); diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 9eb927784..87e0195c3 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Png try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Png try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Png public IImageInfo Identify(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Identify(bufferedStream); } @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Formats.Png public Task IdentifyAsync(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.IdentifyAsync(bufferedStream); } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 89fa4e63d..89caac3f6 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; @@ -1027,7 +1026,7 @@ namespace SixLabors.ImageSharp.Formats.Png private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) { using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var bufferedStream = new BufferedReadStream(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/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 3d9b9a3d2..25aa233db 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Tga try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return decoder.Decode(bufferedStream); } catch (InvalidMemoryOperationException ex) @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Tga try { - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); } catch (InvalidMemoryOperationException ex) @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return new TgaDecoderCore(configuration, this).Identify(bufferedStream); } @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Tga { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(stream); + using var bufferedStream = new BufferedReadStream(configuration, stream); return new TgaDecoderCore(configuration, this).IdentifyAsync(bufferedStream); } } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 816626374..02015eb56 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -14,12 +14,7 @@ namespace SixLabors.ImageSharp.IO /// internal sealed class BufferedReadStream : Stream { - /// - /// The length, in bytes, of the underlying buffer. - /// - public const int BufferLength = 8192; - - private const int MaxBufferIndex = BufferLength - 1; + private readonly int maxBufferIndex; private readonly byte[] readBuffer; @@ -38,9 +33,11 @@ namespace SixLabors.ImageSharp.IO /// /// Initializes a new instance of the class. /// + /// The configuration which allows altering default behaviour or extending the library. /// The input stream. - public BufferedReadStream(Stream 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."); @@ -53,10 +50,11 @@ namespace SixLabors.ImageSharp.IO } this.BaseStream = stream; - this.Position = (int)stream.Position; this.Length = stream.Length; - - this.readBuffer = ArrayPool.Shared.Rent(BufferLength); + 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 { @@ -64,7 +62,16 @@ namespace SixLabors.ImageSharp.IO } // This triggers a full read on first attempt. - this.readBufferIndex = BufferLength; + this.readBufferIndex = this.BufferSize; + } + + /// + /// Gets the size, in bytes, of the underlying buffer. + /// + public int BufferSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; } /// @@ -79,6 +86,9 @@ namespace SixLabors.ImageSharp.IO [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)) @@ -91,7 +101,7 @@ namespace SixLabors.ImageSharp.IO // Base stream seek will throw for us if invalid. this.BaseStream.Seek(value, SeekOrigin.Begin); this.readerPosition = value; - this.readBufferIndex = BufferLength; + this.readBufferIndex = this.BufferSize; } } } @@ -125,7 +135,7 @@ namespace SixLabors.ImageSharp.IO // Our buffer has been read. // We need to refill and start again. - if (this.readBufferIndex > MaxBufferIndex) + if (this.readBufferIndex > this.maxBufferIndex) { this.FillReadBuffer(); } @@ -142,14 +152,14 @@ namespace SixLabors.ImageSharp.IO public override int Read(byte[] buffer, int offset, int count) { // Too big for our buffer. Read directly from the stream. - if (count > BufferLength) + 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 > BufferLength) + if (count + this.readBufferIndex > this.BufferSize) { return this.ReadToBufferViaCopySlow(buffer, offset, count); } @@ -164,14 +174,14 @@ namespace SixLabors.ImageSharp.IO { // Too big for our buffer. Read directly from the stream. int count = buffer.Length; - if (count > BufferLength) + 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 > BufferLength) + if (count + this.readBufferIndex > this.BufferSize) { return this.ReadToBufferViaCopySlow(buffer); } @@ -192,7 +202,7 @@ namespace SixLabors.ImageSharp.IO } // Reset to trigger full read on next attempt. - this.readBufferIndex = BufferLength; + this.readBufferIndex = this.BufferSize; } /// @@ -249,7 +259,7 @@ namespace SixLabors.ImageSharp.IO private bool IsInReadBuffer(long newPosition, out long index) { index = newPosition - this.readerPosition + this.readBufferIndex; - return index > -1 && index < BufferLength; + return index > -1 && index < this.BufferSize; } [MethodImpl(MethodImplOptions.NoInlining)] @@ -267,10 +277,10 @@ namespace SixLabors.ImageSharp.IO int i; do { - i = baseStream.Read(this.readBuffer, n, BufferLength - n); + i = baseStream.Read(this.readBuffer, n, this.BufferSize - n); n += i; } - while (n < BufferLength && i > 0); + while (n < this.BufferSize && i > 0); this.readBufferIndex = 0; } diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index beec0b188..fae88f21e 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -687,7 +687,7 @@ 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); + stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; return action(memoryStream); @@ -729,7 +729,7 @@ namespace SixLabors.ImageSharp } using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize).ConfigureAwait(false); memoryStream.Position = 0; return await action(memoryStream).ConfigureAwait(false); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index ef098e263..8c597a8c5 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg public void ParseStreamPdfJs() { using var memoryStream = new MemoryStream(this.jpegBytes); - using var bufferedStream = new BufferedReadStream(memoryStream); + using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true }); decoder.ParseStream(bufferedStream); diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs index 72cceae90..be232c78d 100644 --- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs @@ -35,8 +35,8 @@ namespace SixLabors.ImageSharp.Benchmarks.IO this.stream4 = new MemoryStream(this.buffer); this.stream5 = new MemoryStream(this.buffer); this.stream6 = new MemoryStream(this.buffer); - this.bufferedStream1 = new BufferedReadStream(this.stream3); - this.bufferedStream2 = new BufferedReadStream(this.stream4); + this.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); } @@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Benchmarks.IO private static byte[] CreateTestBytes() { - var buffer = new byte[BufferedReadStream.BufferLength * 3]; + var buffer = new byte[Configuration.Default.StreamProcessingBufferSize * 3]; var random = new Random(); random.NextBytes(buffer); diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index b18d04834..655e98c7f 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/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 0694a0855..912f606b2 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { byte[] bytes = TestFile.Create(TestImages.Jpeg.Progressive.Progress).Bytes; using var ms = new MemoryStream(bytes); - using var bufferedStream = new BufferedReadStream(ms); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); decoder.ParseStream(bufferedStream); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 1d200592a..662ea9e33 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; using var ms = new MemoryStream(sourceBytes); - using var bufferedStream = new BufferedReadStream(ms); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); decoder.ParseStream(bufferedStream); var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; using var ms = new MemoryStream(sourceBytes); - using var bufferedStream = new BufferedReadStream(ms); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); decoder.ParseStream(bufferedStream); var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 96d85fd8e..c6f4704f0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -193,7 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils { byte[] bytes = TestFile.Create(testFileName).Bytes; using var ms = new MemoryStream(bytes); - using var bufferedStream = new BufferedReadStream(ms); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); decoder.ParseStream(bufferedStream, metaDataOnly); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 6284191f3..1ec7e2448 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/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs index d08d5adef..6ceaca012 100644 --- a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs @@ -10,18 +10,36 @@ namespace SixLabors.ImageSharp.Tests.IO { public class BufferedReadStreamTests { - [Fact] - public void BufferedStreamCanReadSingleByteFromOrigin() + 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) { - using (MemoryStream stream = this.CreateTestStream()) + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) { byte[] expected = stream.ToArray(); - using (var reader = new BufferedReadStream(stream)) + 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.Equal(BufferedReadStream.BufferLength, stream.Position); + Assert.True(stream.Position >= bufferSize); Assert.Equal(1, reader.Position); } @@ -30,21 +48,23 @@ namespace SixLabors.ImageSharp.Tests.IO } } - [Fact] - public void BufferedStreamCanReadSingleByteFromOffset() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSingleByteFromOffset(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) { byte[] expected = stream.ToArray(); - const int offset = 5; - using (var reader = new BufferedReadStream(stream)) + 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(BufferedReadStream.BufferLength + offset, stream.Position); + Assert.Equal(bufferSize + offset, stream.Position); Assert.Equal(offset + 1, reader.Position); } @@ -52,33 +72,35 @@ namespace SixLabors.ImageSharp.Tests.IO } } - [Fact] - public void BufferedStreamCanReadSubsequentSingleByteCorrectly() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentSingleByteCorrectly(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) { byte[] expected = stream.ToArray(); int i; - using (var reader = new BufferedReadStream(stream)) + 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 < BufferedReadStream.BufferLength) + if (i < bufferSize) { - Assert.Equal(stream.Position, BufferedReadStream.BufferLength); + Assert.Equal(stream.Position, bufferSize); } - else if (i >= BufferedReadStream.BufferLength && i < BufferedReadStream.BufferLength * 2) + else if (i >= bufferSize && i < bufferSize * 2) { // We should have advanced to the second chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); + Assert.Equal(stream.Position, bufferSize * 2); } else { // We should have advanced to the third chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); + Assert.Equal(stream.Position, bufferSize * 3); } } } @@ -87,109 +109,143 @@ namespace SixLabors.ImageSharp.Tests.IO } } - [Fact] - public void BufferedStreamCanReadMultipleBytesFromOrigin() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadMultipleBytesFromOrigin(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + 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(stream)) + 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.Equal(stream.Position, BufferedReadStream.BufferLength); + Assert.True(stream.Position >= bufferSize); Assert.Equal(buffer.Length, reader.Position); } } } - [Fact] - public void BufferedStreamCanReadSubsequentMultipleByteCorrectly() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentMultipleByteCorrectly(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + 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(stream)) + using (var reader = new BufferedReadStream(this.configuration, stream)) { - for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) + for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment) { - Assert.Equal(2, reader.Read(buffer, 0, 2)); + // 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 + 2, reader.Position); + Assert.Equal(o + increment, reader.Position); - int offset = i * 2; - if (offset < BufferedReadStream.BufferLength) - { - Assert.Equal(stream.Position, BufferedReadStream.BufferLength); - } - else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) + // 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 should have advanced to the second chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); + // We've read an entire chunk once and are + // now reading from that chunk. + Assert.True(stream.Position >= bufferSize); + continue; } - else + + // Second chunk + if (offset < bufferSize * 2) { - // We should have advanced to the third chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); + 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); } } } } - [Fact] - public void BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + 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(stream)) + using (var reader = new BufferedReadStream(this.configuration, stream)) { - for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) + for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment) { - Assert.Equal(2, reader.Read(buffer, 0, 2)); + // 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 + 2, reader.Position); + Assert.Equal(o + increment, reader.Position); - int offset = i * 2; - if (offset < BufferedReadStream.BufferLength) - { - Assert.Equal(stream.Position, BufferedReadStream.BufferLength); - } - else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) + // 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 should have advanced to the second chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); + // We've read an entire chunk once and are + // now reading from that chunk. + Assert.True(stream.Position >= bufferSize); + continue; } - else + + // Second chunk + if (offset < bufferSize * 2) { - // We should have advanced to the third chunk now. - Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); + 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); } } } } - [Fact] - public void BufferedStreamCanSkip() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSkip(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 4)) { byte[] expected = stream.ToArray(); - using (var reader = new BufferedReadStream(stream)) + using (var reader = new BufferedReadStream(this.configuration, stream)) { - int skip = 50; + int skip = 1; int plusOne = 1; - int skip2 = BufferedReadStream.BufferLength; + int skip2 = bufferSize; // Skip reader.Skip(skip); @@ -212,22 +268,25 @@ namespace SixLabors.ImageSharp.Tests.IO } } - [Fact] - public void BufferedStreamReadsSmallStream() + [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(BufferedReadStream.BufferLength / 4)) + using (MemoryStream stream = this.CreateTestStream(Math.Max(1, bufferSize / 4))) { byte[] expected = stream.ToArray(); - const int offset = 5; - using (var reader = new BufferedReadStream(stream)) + 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(BufferedReadStream.BufferLength / 4, stream.Position); + Assert.Equal(Math.Max(1, bufferSize / 4), stream.Position); Assert.Equal(offset + 1, reader.Position); } @@ -235,13 +294,15 @@ namespace SixLabors.ImageSharp.Tests.IO } } - [Fact] - public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin(int bufferSize) { - using (MemoryStream stream = this.CreateTestStream()) + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 3)) { byte[] expected = stream.ToArray(); - using (var reader = new BufferedReadStream(stream)) + using (var reader = new BufferedReadStream(this.configuration, stream)) { for (int i = 0; i < expected.Length; i++) { @@ -251,7 +312,22 @@ namespace SixLabors.ImageSharp.Tests.IO } } - private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3) + [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();