Browse Source

Merge branch 'master' into webp

pull/1552/head
Brian Popow 6 years ago
committed by GitHub
parent
commit
6c76364bf1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      .github/CONTRIBUTING.md
  2. 4
      .github/ISSUE_TEMPLATE/config.yml
  3. 8
      Directory.Build.targets
  4. 20
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  5. 23
      src/ImageSharp/Configuration.cs
  6. 35
      src/ImageSharp/Formats/Bmp/BmpDecoder.cs
  7. 10
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  8. 72
      src/ImageSharp/Formats/Bmp/ImageExtensions.cs
  9. 38
      src/ImageSharp/Formats/Gif/GifDecoder.cs
  10. 8
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  11. 78
      src/ImageSharp/Formats/Gif/ImageExtensions.cs
  12. 9
      src/ImageSharp/Formats/Gif/LzwDecoder.cs
  13. 15
      src/ImageSharp/Formats/IImageDecoderInternals.cs
  14. 43
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  15. 4
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
  16. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  17. 72
      src/ImageSharp/Formats/Jpeg/ImageExtensions.cs
  18. 45
      src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
  19. 159
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  20. 72
      src/ImageSharp/Formats/Png/ImageExtensions.cs
  21. 53
      src/ImageSharp/Formats/Png/PngDecoder.cs
  22. 14
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  23. 9
      src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
  24. 68
      src/ImageSharp/Formats/Tga/ImageExtensions.cs
  25. 31
      src/ImageSharp/Formats/Tga/TgaDecoder.cs
  26. 11
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  27. 423
      src/ImageSharp/IO/BufferedReadStream.cs
  28. 255
      src/ImageSharp/IO/DoubleBufferedStreamReader.cs
  29. 13
      src/ImageSharp/Image.Decode.cs
  30. 42
      src/ImageSharp/Image.FromStream.cs
  31. 11
      src/ImageSharp/Image.cs
  32. 8
      tests/Directory.Build.targets
  33. 23
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  34. 4
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs
  35. 153
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
  36. 279
      tests/ImageSharp.Benchmarks/General/IO/BufferedReadStreamWrapper.cs
  37. 206
      tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs
  38. 1
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  39. 23
      tests/ImageSharp.Tests/ConfigurationTests.cs
  40. 155
      tests/ImageSharp.Tests/Formats/Bmp/ImageExtensionsTest.cs
  41. 155
      tests/ImageSharp.Tests/Formats/Gif/ImageExtensionsTest.cs
  42. 155
      tests/ImageSharp.Tests/Formats/Jpg/ImageExtensionsTest.cs
  43. 20
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  44. 23
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  45. 15
      tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
  46. 155
      tests/ImageSharp.Tests/Formats/Png/ImageExtensionsTest.cs
  47. 2
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
  48. 155
      tests/ImageSharp.Tests/Formats/Tga/ImageExtensionsTest.cs
  49. 2
      tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs
  50. 354
      tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
  51. 176
      tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs
  52. 12
      tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs
  53. 3
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

18
.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:

4
.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.

8
Directory.Build.targets

@ -25,12 +25,16 @@
<!--Src Dependencies-->
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Update="MinVer" PrivateAssets="All" Version="2.3.0" />
<PackageReference Update="System.Buffers" Version="4.5.0" />
<PackageReference Update="System.Buffers" Version="4.5.1" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" />
<PackageReference Update="System.IO.UnmanagedMemoryStream" Version="4.3.0" />
<PackageReference Update="System.Numerics.Vectors" Version="4.5.0" />
<!--
Do no update System.Memory as it currently breaks the CI build
with FileNotFoundException for SixLabors.ImageSharp.Tests.dll.config
-->
<PackageReference Update="System.Memory" Version="4.5.3" />
<PackageReference Update="System.Runtime.CompilerServices.Unsafe" Version="4.7" />
<PackageReference Update="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
<PackageReference Update="System.Threading.Tasks.Parallel" Version="4.3.0" />
<PackageReference Update="System.ValueTuple" Version="4.5.0" />

20
src/ImageSharp/Common/Extensions/StreamExtensions.cs

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.IO;
using SixLabors.ImageSharp.Memory;
#if !SUPPORTS_SPAN_STREAM
using System.Buffers;
#endif
namespace SixLabors.ImageSharp
{
@ -40,7 +38,7 @@ namespace SixLabors.ImageSharp
/// Skips the number of bytes in the given stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">The count.</param>
/// <param name="count">A byte offset relative to the origin parameter.</param>
public static void Skip(this Stream stream, int count)
{
if (count < 1)
@ -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<byte>.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<byte>.Shared.Return(buffer);
}
}
public static void Read(this Stream stream, IManagedByteBuffer buffer)

23
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.
/// </summary>
private static readonly Lazy<Configuration> Lazy = new Lazy<Configuration>(CreateDefaultInstance);
private const int DefaultStreamProcessingBufferSize = 8096;
private int streamProcessingBufferSize = DefaultStreamProcessingBufferSize;
private int maxDegreeOfParallelism = Environment.ProcessorCount;
/// <summary>
@ -76,6 +76,24 @@ namespace SixLabors.ImageSharp
}
}
/// <summary>
/// Gets or sets the size of the buffer to use when working with streams.
/// Intitialized with <see cref="DefaultStreamProcessingBufferSize"/> by default.
/// </summary>
public int StreamProcessingBufferSize
{
get => this.streamProcessingBufferSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(this.StreamProcessingBufferSize));
}
this.streamProcessingBufferSize = value;
}
}
/// <summary>
/// Gets a set of properties for the Congiguration.
/// </summary>
@ -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,

35
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;
/// <inheritdoc/>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
@ -38,19 +39,24 @@ namespace SixLabors.ImageSharp.Formats.Bmp
try
{
return await decoder.DecodeAsync<TPixel>(stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.Decode<TPixel>(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);
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream)
=> this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
@ -58,28 +64,28 @@ namespace SixLabors.ImageSharp.Formats.Bmp
try
{
return decoder.Decode<TPixel>(stream);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return await decoder.DecodeAsync<TPixel>(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);
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream)
=> await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
@ -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);
}
}
}

10
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
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream stream;
private BufferedReadStream stream;
/// <summary>
/// The metadata.
@ -120,7 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(Stream stream)
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
try
@ -199,7 +197,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <inheritdoc />
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
/// </summary>
/// <returns>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.</returns>
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;

72
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
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null);
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsBmp(this Image source, Stream stream) => SaveAsBmp(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsBmp(this Image source, Stream stream) => source.SaveAsBmp(stream, null);
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsBmpAsync(this Image source, Stream stream) => SaveAsBmpAsync(source, stream, null);
/// <summary>
/// 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));
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance));
}
}
}

38
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;
/// <inheritdoc/>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new GifDecoderCore(configuration, this);
try
{
return await decoder.DecodeAsync<TPixel>(stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.Decode<TPixel>(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;
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream)
=> this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new GifDecoderCore(configuration, this);
try
{
return decoder.Decode<TPixel>(stream);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return await decoder.DecodeAsync<TPixel>(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;
}
}
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream)
=> await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
@ -82,13 +94,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(stream, nameof(stream));
var decoder = new GifDecoderCore(configuration, this);
return decoder.IdentifyAsync(stream);
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.IdentifyAsync(bufferedStream);
}
}
}

8
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// The currently loaded stream.
/// </summary>
private Stream stream;
private BufferedReadStream stream;
/// <summary>
/// The global color table.
@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator;
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(Stream stream)
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> image = null;
@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
/// <inheritdoc />
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
/// </summary>
/// <param name="stream">The stream containing image data. </param>
private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream)
private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{
this.stream = stream;

78
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
{
/// <summary>
/// Saves the image to the given stream in the gif format.
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null);
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsGif(this Image source, string path, GifEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsGif(this Image source, Stream stream) => SaveAsGif(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsGif(this Image source, Stream stream) => source.SaveAsGif(stream, null);
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsGifAsync(this Image source, Stream stream) => SaveAsGifAsync(source, stream, null);
/// <summary>
/// Saves the image to the given stream in the gif format.
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The options for the encoder.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) =>
source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance));
}
}
}

9
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
/// <summary>
/// The stream to decode.
/// </summary>
private readonly Stream stream;
private readonly BufferedReadStream stream;
/// <summary>
/// The prefix buffer.
@ -52,8 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
/// <param name="stream">The stream to read from.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="stream"/> is null.</exception>
public LzwDecoder(MemoryAllocator memoryAllocator, Stream stream)
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));

15
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
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <exception cref="System.ArgumentNullException">
/// <para><paramref name="stream"/> is null.</para>
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
/// <returns>The decoded image.</returns>
Image<TPixel> Decode<TPixel>(Stream stream)
Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>;
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <returns>The <see cref="IImageInfo"/>.</returns>
IImageInfo Identify(Stream stream);
IImageInfo Identify(BufferedReadStream stream);
}
}

43
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.
/// </summary>
/// <param name="decoder">The decoder.</param>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public static async Task<IImageInfo> 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);
}
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task<IImageInfo> IdentifyAsync(this IImageDecoderInternals decoder, BufferedReadStream stream)
=> Task.FromResult(decoder.Identify(stream));
/// <summary>
/// Decodes the image from the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="decoder">The decoder.</param>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <exception cref="System.ArgumentNullException">
/// <para><paramref name="stream"/> is null.</para>
/// </exception>
/// <returns>The decoded image.</returns>
public static async Task<Image<TPixel>> DecodeAsync<TPixel>(this IImageDecoderInternals decoder, Stream stream)
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task<Image<TPixel>> DecodeAsync<TPixel>(this IImageDecoderInternals decoder, BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
if (stream.CanSeek)
{
return decoder.Decode<TPixel>(stream);
}
using MemoryStream ms = decoder.Configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
await stream.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
return decoder.Decode<TPixel>(ms);
}
=> Task.FromResult(decoder.Decode<TPixel>(stream));
}
}

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
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;

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

72
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
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null);
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The options for the encoder.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsJpegAsync(this Image source, Stream stream) => SaveAsJpegAsync(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) =>
source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the jpeg format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance));
}
}
}

45
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
/// </summary>
public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfoDetector
{
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.Decode<TPixel>(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;
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream)
=> this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(stream);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return await decoder.DecodeAsync<TPixel>(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;
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream)
=> this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream)
=> await this.DecodeAsync<Rgba32>(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);
}
/// <inheritdoc/>
public async Task<IImageInfo> IdentifyAsync(Configuration configuration, Stream stream)
public Task<IImageInfo> 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);
}
}
}

159
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
/// </summary>
public int BitsPerPixel => this.ComponentCount * this.Frame.Precision;
/// <summary>
/// Gets the input stream.
/// </summary>
public DoubleBufferedStreamReader InputStream { get; private set; }
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
@ -180,7 +173,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="marker">The buffer to read file markers to</param>
/// <param name="stream">The input stream</param>
/// <returns>The <see cref="JpegFileMarker"/></returns>
public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStreamReader stream)
public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream)
{
int value = stream.Read(marker, 0, 2);
@ -212,7 +205,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Stream stream)
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ParseStream(stream);
@ -224,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <inheritdoc/>
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
/// </summary>
/// <param name="stream">The input stream</param>
/// <param name="metadataOnly">Whether to decode metadata only.</param>
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);
}
}
/// <inheritdoc/>
public void Dispose()
{
this.InputStream?.Dispose();
this.Frame?.Dispose();
// Set large fields to null.
this.InputStream = null;
this.Frame = null;
this.dcHuffmanTables = null;
this.acHuffmanTables = null;
@ -504,18 +494,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Processes the application header containing the JFIF identifier plus extra data.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApplicationHeaderMarker(int remaining)
private void ProcessApplicationHeaderMarker(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);
}
}
/// <summary>
/// Processes the App1 marker retrieving any stored metadata
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp1Marker(int remaining)
private void ProcessApp1Marker(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
/// <summary>
/// Processes the App2 marker retrieving any stored ICC profile information
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp2Marker(int remaining)
private void ProcessApp2Marker(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.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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<byte> 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.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp14Marker(int remaining)
private void ProcessApp14Marker(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);
}
}
/// <summary>
/// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the tables do not match the header
/// </exception>
private void ProcessDefineQuantizationTablesMarker(int remaining)
private void ProcessDefineQuantizationTablesMarker(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
/// <summary>
/// Processes the Start of Frame marker. Specified in section B.2.2.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <param name="frameMarker">The current frame marker.</param>
/// <param name="metadataOnly">Whether to parse metadata only</param>
private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
private void ProcessStartOfFrameMarker(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.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessDefineHuffmanTablesMarker(int remaining)
private void ProcessDefineHuffmanTablesMarker(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
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessDefineRestartIntervalMarker(int remaining)
private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining)
{
if (remaining != 2)
{
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining);
}
this.resetInterval = this.ReadUint16();
this.resetInterval = this.ReadUint16(stream);
}
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
private void ProcessStartOfScanMarker()
/// <param name="stream">The input stream.</param>
private void ProcessStartOfScanMarker(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
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes
/// </summary>
/// <param name="stream">The input stream.</param>
/// <returns>The <see cref="ushort"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
private ushort ReadUint16()
private ushort ReadUint16(BufferedReadStream stream)
{
this.InputStream.Read(this.markerBuffer, 0, 2);
stream.Read(this.markerBuffer, 0, 2);
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}

72
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
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null);
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsPng(this Image source, string path, PngEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The options for the encoder.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsPngAsync(this Image source, Stream stream) => SaveAsPngAsync(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) =>
source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance));
}
}
}

53
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
/// </summary>
public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDetector
{
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <summary>
/// Decodes the image from the specified stream to the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration for the image.</param>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <returns>The decoded image.</returns>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new PngDecoderCore(configuration, this);
try
{
return await decoder.DecodeAsync<TPixel>(stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.Decode<TPixel>(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;
}
}
/// <summary>
/// Decodes the image from the specified stream to the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration for the image.</param>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new PngDecoderCore(configuration, this);
try
{
return decoder.Decode<TPixel>(stream);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return await decoder.DecodeAsync<TPixel>(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;
}
}
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public Task<IImageInfo> 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);
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
}
}

14
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
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream currentStream;
private BufferedReadStream currentStream;
/// <summary>
/// The png header.
@ -132,7 +131,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public Size Dimensions => new Size(this.header.Width, this.header.Height);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Stream stream)
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var metadata = new ImageMetadata();
@ -224,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
/// <inheritdoc/>
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
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The image to decode to.</param>
/// <param name="pngMetadata">The png metadata</param>
private void DecodePixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata)
private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
while (this.currentRow < this.header.Height)
@ -555,7 +554,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The current image.</param>
/// <param name="pngMetadata">The png metadata.</param>
private void DecodeInterlacedPixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata)
private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
int pass = 0;
@ -1027,7 +1026,8 @@ namespace SixLabors.ImageSharp.Formats.Png
private bool TryUncompressTextData(ReadOnlySpan<byte> 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))
{

9
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
/// <summary>
/// The inner raw memory stream.
/// </summary>
private readonly Stream innerStream;
private readonly BufferedReadStream innerStream;
/// <summary>
/// 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 <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(Stream innerStream)
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(Stream innerStream, Func<int> getData)
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> 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;

68
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
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null);
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
@ -26,11 +68,33 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The options for the encoder.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, Stream stream) => SaveAsTgaAsync(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) =>
source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance));
}
}

31
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
{
/// <inheritdoc/>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
@ -23,21 +24,26 @@ namespace SixLabors.ImageSharp.Formats.Tga
try
{
return await decoder.DecodeAsync<TPixel>(stream).ConfigureAwait(false);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return decoder.Decode<TPixel>(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;
}
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream)
=> this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
public async Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
@ -46,13 +52,14 @@ namespace SixLabors.ImageSharp.Formats.Tga
try
{
return decoder.Decode<TPixel>(stream);
using var bufferedStream = new BufferedReadStream(configuration, stream);
return await decoder.DecodeAsync<TPixel>(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
}
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc />
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream)
=> await this.DecodeAsync<Rgba32>(configuration, stream).ConfigureAwait(false);
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
@ -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);
}
}
}

11
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
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream currentStream;
private BufferedReadStream currentStream;
/// <summary>
/// The bitmap decoder options.
@ -78,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(Stream stream)
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
try
@ -641,7 +640,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
}
/// <inheritdoc />
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
/// <summary>
/// Reads the tga file header from the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <returns>The image origin.</returns>
private TgaImageOrigin ReadFileHeader(Stream stream)
private TgaImageOrigin ReadFileHeader(BufferedReadStream stream)
{
this.currentStream = stream;

423
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
{
/// <summary>
/// A readonly stream that add a secondary level buffer in addition to native stream
/// buffered reading to reduce the overhead of small incremental reads.
/// </summary>
internal sealed 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;
/// <summary>
/// Initializes a new instance of the <see cref="BufferedReadStream"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="stream">The input stream.</param>
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<byte>.Shared.Rent(this.BufferSize);
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin();
unsafe
{
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
}
// This triggers a full read on first attempt.
this.readBufferIndex = this.BufferSize;
}
/// <summary>
/// Gets the size, in bytes, of the underlying buffer.
/// </summary>
public int BufferSize
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
/// <inheritdoc/>
public override long Length { get; }
/// <inheritdoc/>
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;
}
}
}
/// <inheritdoc/>
public override bool CanRead { get; } = true;
/// <inheritdoc/>
public override bool CanSeek { get; } = true;
/// <inheritdoc/>
public override bool CanWrite { get; } = false;
/// <summary>
/// Gets the underlying stream.
/// </summary>
public Stream BaseStream
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
/// <inheritdoc/>
[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++];
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count)
{
// Too big for our buffer. Read directly from the stream.
if (count > 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
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(Span<byte> 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
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
[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;
}
/// <inheritdoc/>
/// <exception cref="NotSupportedException">
/// This operation is not supported in <see cref="BufferedReadStream"/>.
/// </exception>
public override void SetLength(long value)
=> throw new NotSupportedException();
/// <inheritdoc/>
/// <exception cref="NotSupportedException">
/// This operation is not supported in <see cref="BufferedReadStream"/>.
/// </exception>
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.readBufferHandle.Dispose();
ArrayPool<byte>.Shared.Return(this.readBuffer);
this.Flush();
base.Dispose(true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private 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<byte> 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<byte> 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<byte> 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);
}
}
}
}

255
src/ImageSharp/IO/DoubleBufferedStreamReader.cs

@ -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
{
/// <summary>
/// A stream reader that add a secondary level buffer in addition to native stream buffered reading
/// to reduce the overhead of small incremental reads.
/// </summary>
internal sealed unsafe class DoubleBufferedStreamReader : IDisposable
{
/// <summary>
/// The length, in bytes, of the buffering chunk.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="DoubleBufferedStreamReader"/> class.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
/// <param name="stream">The input stream.</param>
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;
}
/// <summary>
/// Gets the length, in bytes, of the stream.
/// </summary>
public long Length => this.length;
/// <summary>
/// Gets or sets the current position within the stream.
/// </summary>
public long Position
{
get => this.position;
set
{
// Only reset 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;
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The unsigned byte cast to an <see cref="int"/>, or -1 if at the end of the stream.</returns>
[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++];
}
/// <summary>
/// Skips the number of bytes in the stream
/// </summary>
/// <param name="count">The number of bytes to skip.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void Skip(int count) => this.Position += count;
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position within the stream
/// by the number of bytes read.
/// </summary>
/// <param name="buffer">
/// 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.
/// </param>
/// <param name="offset">
/// The zero-based byte offset in buffer at which to begin storing the data read
/// from the current stream.
/// </param>
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
/// <returns>
/// 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.
/// </returns>
[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;
}
/// <inheritdoc/>
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);
}
}
}
}

13
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

42
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<T>(Configuration configuration, Stream stream, Func<Stream, T> action)
/// <summary>
/// Performs the given action against the stream ensuring that it is seekable.
/// </summary>
/// <typeparam name="T">The type of object returned from the action.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="stream">The input stream.</param>
/// <param name="action">The action to perform.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
private static T WithSeekableStream<T>(
Configuration configuration,
Stream stream,
Func<Stream, T> 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);
}
/// <summary>
/// Performs the given action asynchronously against the stream ensuring that it is seekable.
/// </summary>
/// <typeparam name="T">The type of object returned from the action.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="stream">The input stream.</param>
/// <param name="action">The action to perform.</param>
/// <returns>The <see cref="Task{T}"/>.</returns>
private static async Task<T> WithSeekableStreamAsync<T>(
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);
}
}
}

11
src/ImageSharp/Image.cs

@ -103,15 +103,15 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream or encoder is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if the stream or encoder is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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));
}
/// <summary>
@ -179,9 +179,8 @@ namespace SixLabors.ImageSharp
public void Visit<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> => this.encoder.Encode(image, this.stream);
public async Task VisitAsync<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
=> await this.encoder.EncodeAsync(image, this.stream).ConfigureAwait(false);
public Task VisitAsync<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> => this.encoder.EncodeAsync(image, this.stream);
}
}
}

8
tests/Directory.Build.targets

@ -25,14 +25,14 @@
<ItemGroup>
<!--Test Dependencies-->
<PackageReference Update="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Update="Colourful" Version="2.0.3" />
<PackageReference Update="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.1" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Update="Colourful" Version="2.0.5" />
<PackageReference Update="coverlet.collector" Version="1.3.0" PrivateAssets="All"/>
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.15.5" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="5.0.0-beta.20069.1" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.5.0-preview-20200116-01" />
<PackageReference Update="Moq" Version="4.10.0" />
<PackageReference Update="Moq" Version="4.14.5" />
<PackageReference Update="Pfim" Version="0.9.1" />
<PackageReference Update="SharpZipLib" Version="1.2.0" />
<PackageReference Update="System.Drawing.Common" Version="4.7.0" />

23
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):

4
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,

153
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs

@ -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 | - | - | - | - |
*/
}

279
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
{
/// <summary>
/// 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.
/// </summary>
internal sealed unsafe class BufferedReadStreamWrapper : IDisposable
{
/// <summary>
/// The length, in bytes, of the underlying buffer.
/// </summary>
public const int BufferLength = 8192;
private const int MaxBufferIndex = BufferLength - 1;
private readonly Stream stream;
private readonly byte[] readBuffer;
private MemoryHandle readBufferHandle;
private readonly byte* pinnedReadBuffer;
// 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;
/// <summary>
/// Initializes a new instance of the <see cref="BufferedReadStreamWrapper"/> class.
/// </summary>
/// <param name="stream">The input stream.</param>
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<byte>.Shared.Rent(BufferLength);
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin();
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer;
// This triggers a full read on first attempt.
this.readBufferIndex = BufferLength;
}
/// <summary>
/// Gets the length, in bytes, of the stream.
/// </summary>
public long Length { get; }
/// <summary>
/// Gets or sets the current position within the stream.
/// </summary>
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;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.readBufferHandle.Dispose();
ArrayPool<byte>.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);
}
}
}
}

206
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 | - | - | - | - |
*/
}

1
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -32,7 +32,6 @@
<!-- Exclude benchmarks using internals, in case of unsigned benchmark execution: -->
<ItemGroup Condition="'$(SignAssembly)' == 'false'">
<Compile Remove="Codecs\Jpeg\BlockOperations\**" />
<Compile Remove="Codecs\Jpeg\DoubleBufferedStreams.cs" />
<Compile Remove="Codecs\Jpeg\YCbCrColorConversion.cs" />
<Compile Remove="Codecs\Jpeg\DecodeJpegParseStreamOnly.cs" />
<Compile Remove="Color\Bulk\**" />

23
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -76,10 +76,7 @@ namespace SixLabors.ImageSharp.Tests
if (throws)
{
Assert.Throws<ArgumentOutOfRangeException>(
() =>
{
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<ArgumentOutOfRangeException>(
() => config.StreamProcessingBufferSize = 0);
}
}
}

155
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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(10, 10))
{
await image.SaveAsBmpAsync(memoryStream, new BmpEncoder());
}
memoryStream.Position = 0;
using (Image.Load(memoryStream, out IImageFormat mime))
{
Assert.Equal("image/bmp", mime.DefaultMimeType);
}
}
}
}

155
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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(10, 10))
{
await image.SaveAsGifAsync(memoryStream, new GifEncoder());
}
memoryStream.Position = 0;
using (Image.Load(memoryStream, out IImageFormat mime))
{
Assert.Equal("image/gif", mime.DefaultMimeType);
}
}
}
}

155
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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(10, 10))
{
await image.SaveAsJpegAsync(memoryStream, new JpegEncoder());
}
memoryStream.Position = 0;
using (Image.Load(memoryStream, out IImageFormat mime))
{
Assert.Equal("image/jpeg", mime.DefaultMimeType);
}
}
}
}

20
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";

23
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<TPixel>(

15
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;
}
}
}

155
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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(10, 10))
{
await image.SaveAsPngAsync(memoryStream, new PngEncoder());
}
memoryStream.Position = 0;
using (Image.Load(memoryStream, out IImageFormat mime))
{
Assert.Equal("image/png", mime.DefaultMimeType);
}
}
}
}

2
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<InvalidImageContentException>(() => decoder.Decode<Rgb24>(null, memStream));
Assert.Throws<InvalidImageContentException>(() => decoder.Decode<Rgb24>(Configuration.Default, memStream));
Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message);
}

155
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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(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<Rgba32>(10, 10))
{
await image.SaveAsTgaAsync(memoryStream, new TgaEncoder());
}
memoryStream.Position = 0;
using (Image.Load(memoryStream, out IImageFormat mime))
{
Assert.Equal("image/tga", mime.DefaultMimeType);
}
}
}
}

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

354
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<int> BufferSizes =
new TheoryData<int>()
{
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<byte> 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<ArgumentOutOfRangeException>(() => reader.Position = -stream.Length);
Assert.Throws<ArgumentOutOfRangeException>(() => 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);
}
}
}
}

176
tests/ImageSharp.Tests/IO/DoubleBufferedStreamReaderTests.cs

@ -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);
}
}
}

12
tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs

@ -81,9 +81,9 @@ namespace SixLabors.ImageSharp.Tests
{
Assert.Throws<System.IO.FileNotFoundException>(
() =>
{
Image.Load<Rgba32>(this.TopLevelConfiguration, Guid.NewGuid().ToString());
});
{
Image.Load<Rgba32>(this.TopLevelConfiguration, Guid.NewGuid().ToString());
});
}
[Fact]
@ -91,9 +91,9 @@ namespace SixLabors.ImageSharp.Tests
{
Assert.Throws<ArgumentNullException>(
() =>
{
Image.Load<Rgba32>(this.TopLevelConfiguration, (string)null);
});
{
Image.Load<Rgba32>(this.TopLevelConfiguration, (string)null);
});
}
}
}

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

Loading…
Cancel
Save