Browse Source

Re-introduce IImageEncoder and split encoding pipelines.

pull/2301/head
James Jackson-South 4 years ago
parent
commit
c550af8c5a
  1. 8
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  2. 6
      src/ImageSharp/Advanced/AotCompilerTools.cs
  3. 11
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  4. 11
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  5. 26
      src/ImageSharp/Formats/IImageEncoder.cs
  6. 4
      src/ImageSharp/Formats/IImageEncoderInternals.cs
  7. 96
      src/ImageSharp/Formats/ImageDecoder.cs
  8. 56
      src/ImageSharp/Formats/ImageEncoderUtilities.cs
  9. 16
      src/ImageSharp/Formats/ImageFormatManager.cs
  10. 13
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  11. 15
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  12. 14
      src/ImageSharp/Formats/Png/PngEncoder.cs
  13. 23
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  14. 94
      src/ImageSharp/Formats/SynchronousImageEncoder.cs
  15. 13
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  16. 11
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  17. 13
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  18. 8
      src/ImageSharp/Image.cs
  19. 18
      src/ImageSharp/ImageExtensions.cs
  20. 10
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs
  21. 8
      tests/ImageSharp.Tests/Image/ImageSaveTests.cs
  22. 2
      tests/ImageSharp.Tests/Image/ImageTests.Save.cs
  23. 4
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  24. 2
      tests/ImageSharp.Tests/Image/ImageTests.cs
  25. 2
      tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
  26. 2
      tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs
  27. 12
      tests/ImageSharp.Tests/TestFormat.cs
  28. 4
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  29. 32
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs
  30. 11
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs
  31. 8
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  32. 10
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  33. 4
      tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs

8
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -21,8 +21,8 @@ public static class AdvancedImageExtensions
/// <param name="filePath">The target file path to save the image to.</param> /// <param name="filePath">The target file path to save the image to.</param>
/// <exception cref="ArgumentNullException">The file path is null.</exception> /// <exception cref="ArgumentNullException">The file path is null.</exception>
/// <exception cref="NotSupportedException">No encoder available for provided path.</exception> /// <exception cref="NotSupportedException">No encoder available for provided path.</exception>
/// <returns>The matching <see cref="ImageEncoder"/>.</returns> /// <returns>The matching <see cref="IImageEncoder"/>.</returns>
public static ImageEncoder DetectEncoder(this Image source, string filePath) public static IImageEncoder DetectEncoder(this Image source, string filePath)
{ {
Guard.NotNull(filePath, nameof(filePath)); Guard.NotNull(filePath, nameof(filePath));
@ -40,13 +40,13 @@ public static class AdvancedImageExtensions
throw new NotSupportedException(sb.ToString()); throw new NotSupportedException(sb.ToString());
} }
ImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, ImageEncoder> enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders)
{ {
sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine);
} }

6
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -230,7 +230,7 @@ internal static class AotCompilerTools
} }
/// <summary> /// <summary>
/// This method pre-seeds the all <see cref="ImageEncoder"/> in the AoT compiler. /// This method pre-seeds the all <see cref="IImageEncoder"/> in the AoT compiler.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve] [Preserve]
@ -266,14 +266,14 @@ internal static class AotCompilerTools
} }
/// <summary> /// <summary>
/// This method pre-seeds the <see cref="ImageEncoder"/> in the AoT compiler. /// This method pre-seeds the <see cref="IImageEncoder"/> in the AoT compiler.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <typeparam name="TEncoder">The encoder.</typeparam> /// <typeparam name="TEncoder">The encoder.</typeparam>
[Preserve] [Preserve]
private static void AotCompileImageEncoder<TPixel, TEncoder>() private static void AotCompileImageEncoder<TPixel, TEncoder>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TEncoder : ImageEncoder where TEncoder : IImageEncoder
{ {
default(TEncoder).Encode<TPixel>(default, default); default(TEncoder).Encode<TPixel>(default, default);
default(TEncoder).EncodeAsync<TPixel>(default, default, default); default(TEncoder).EncodeAsync<TPixel>(default, default, default);

11
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -24,16 +24,9 @@ public sealed class BmpEncoder : QuantizingImageEncoder
public bool SupportTransparency { get; init; } public bool SupportTransparency { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

11
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -16,16 +16,9 @@ public sealed class GifEncoder : QuantizingImageEncoder
public GifColorTableMode? ColorTableMode { get; init; } public GifColorTableMode? ColorTableMode { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
GifEncoderCore encoder = new(image.GetConfiguration(), this); GifEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
GifEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

26
src/ImageSharp/Formats/ImageEncoder.cs → src/ImageSharp/Formats/IImageEncoder.cs

@ -2,15 +2,13 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
/// The base class for all image encoders. /// Defines the contract for all image encoders.
/// </summary> /// </summary>
public abstract class ImageEncoder public interface IImageEncoder
{ {
/// <summary> /// <summary>
/// Gets a value indicating whether to ignore decoded metadata when encoding. /// Gets a value indicating whether to ignore decoded metadata when encoding.
@ -23,7 +21,7 @@ public abstract class ImageEncoder
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}" /> to encode from.</param> /// <param name="image">The <see cref="Image{TPixel}" /> to encode from.</param>
/// <param name="stream">The <see cref="Stream" /> to encode the image data to.</param> /// <param name="stream">The <see cref="Stream" /> to encode the image data to.</param>
public abstract void Encode<TPixel>(Image<TPixel> image, Stream stream) public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
/// <summary> /// <summary>
@ -34,22 +32,6 @@ public abstract class ImageEncoder
/// <param name="stream">The <see cref="Stream" /> to encode the image data to.</param> /// <param name="stream">The <see cref="Stream" /> to encode the image data to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
public abstract Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
} }
/// <summary>
/// The base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree;
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

4
src/ImageSharp/Formats/IImageEncoderInternals.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -6,7 +6,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
/// Abstraction for shared internals for ***DecoderCore implementations to be used with <see cref="ImageEncoderUtilities"/>. /// Abstraction for shared internals for ***DecoderCore implementations.
/// </summary> /// </summary>
internal interface IImageEncoderInternals internal interface IImageEncoderInternals
{ {

96
src/ImageSharp/Formats/ImageDecoder.cs

@ -13,6 +13,53 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary> /// </summary>
public abstract class ImageDecoder : IImageDecoder public abstract class ImageDecoder : IImageDecoder
{ {
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(
options,
stream,
s => this.Decode<TPixel>(options, s, default));
/// <inheritdoc/>
public Image Decode(DecoderOptions options, Stream stream)
=> WithSeekableStream(
options,
stream,
s => this.Decode(options, s, default));
/// <inheritdoc/>
public Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken);
/// <inheritdoc/>
public Task<Image> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Decode(options, s, ct),
cancellationToken);
/// <inheritdoc/>
public IImageInfo Identify(DecoderOptions options, Stream stream)
=> WithSeekableStream(
options,
stream,
s => this.Identify(options, s, default));
/// <inheritdoc/>
public Task<IImageInfo> IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Identify(options, s, ct),
cancellationToken);
/// <summary> /// <summary>
/// Decodes the image from the specified stream to an <see cref="Image{TPixel}" /> of a specific pixel type. /// Decodes the image from the specified stream to an <see cref="Image{TPixel}" /> of a specific pixel type.
/// </summary> /// </summary>
@ -93,53 +140,6 @@ public abstract class ImageDecoder : IImageDecoder
return currentSize.Width != targetSize.Width && currentSize.Height != targetSize.Height; return currentSize.Width != targetSize.Width && currentSize.Height != targetSize.Height;
} }
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStream(
options,
stream,
s => this.Decode<TPixel>(options, s, default));
/// <inheritdoc/>
public Image Decode(DecoderOptions options, Stream stream)
=> WithSeekableStream(
options,
stream,
s => this.Decode(options, s, default));
/// <inheritdoc/>
public Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Decode<TPixel>(options, s, ct),
cancellationToken);
/// <inheritdoc/>
public Task<Image> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Decode(options, s, ct),
cancellationToken);
/// <inheritdoc/>
public IImageInfo Identify(DecoderOptions options, Stream stream)
=> WithSeekableStream(
options,
stream,
s => this.Identify(options, s, default));
/// <inheritdoc/>
public Task<IImageInfo> IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> WithSeekableStreamAsync(
options,
stream,
(s, ct) => this.Identify(options, s, ct),
cancellationToken);
internal static T WithSeekableStream<T>( internal static T WithSeekableStream<T>(
DecoderOptions options, DecoderOptions options,
Stream stream, Stream stream,
@ -213,7 +213,7 @@ public abstract class ImageDecoder : IImageDecoder
// code below to copy the stream to an in-memory buffer before invoking the action. // code below to copy the stream to an in-memory buffer before invoking the action.
// TODO: Avoid the existing double copy caused by calling IdentifyAsync followed by DecodeAsync. // TODO: Avoid the existing double copy caused by calling IdentifyAsync followed by DecodeAsync.
// Perhaps we can make overloads accepting the chunked memorystream? // Perhaps we can make overloads accepting the chunked memorystream? Or maybe AOT is good with pattern matching against types?
Configuration configuration = options.Configuration; Configuration configuration = options.Configuration;
using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator);
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);

56
src/ImageSharp/Formats/ImageEncoderUtilities.cs

@ -1,56 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
internal static class ImageEncoderUtilities
{
public static async Task EncodeAsync<TPixel>(
this IImageEncoderInternals encoder,
Image<TPixel> image,
Stream stream,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = image.GetConfiguration();
if (stream.CanSeek)
{
await DoEncodeAsync(stream).ConfigureAwait(false);
}
else
{
using MemoryStream ms = new();
await DoEncodeAsync(ms);
ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)
.ConfigureAwait(false);
}
Task DoEncodeAsync(Stream innerStream)
{
try
{
encoder.Encode(image, innerStream, cancellationToken);
return Task.CompletedTask;
}
catch (OperationCanceledException)
{
return Task.FromCanceled(cancellationToken);
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
}
public static void Encode<TPixel>(
this IImageEncoderInternals encoder,
Image<TPixel> image,
Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> encoder.Encode(image, stream, default);
}

16
src/ImageSharp/Formats/ImageFormatManager.cs

@ -17,9 +17,9 @@ public class ImageFormatManager
private static readonly object HashLock = new(); private static readonly object HashLock = new();
/// <summary> /// <summary>
/// The list of supported <see cref="ImageEncoder"/> keyed to mime types. /// The list of supported <see cref="IImageEncoder"/> keyed to mime types.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<IImageFormat, ImageEncoder> mimeTypeEncoders = new(); private readonly ConcurrentDictionary<IImageFormat, IImageEncoder> mimeTypeEncoders = new();
/// <summary> /// <summary>
/// The list of supported <see cref="IImageDecoder"/> keyed to mime types. /// The list of supported <see cref="IImageDecoder"/> keyed to mime types.
@ -64,9 +64,9 @@ public class ImageFormatManager
internal IEnumerable<KeyValuePair<IImageFormat, IImageDecoder>> ImageDecoders => this.mimeTypeDecoders; internal IEnumerable<KeyValuePair<IImageFormat, IImageDecoder>> ImageDecoders => this.mimeTypeDecoders;
/// <summary> /// <summary>
/// Gets the currently registered <see cref="ImageEncoder"/>s. /// Gets the currently registered <see cref="IImageEncoder"/>s.
/// </summary> /// </summary>
internal IEnumerable<KeyValuePair<IImageFormat, ImageEncoder>> ImageEncoders => this.mimeTypeEncoders; internal IEnumerable<KeyValuePair<IImageFormat, IImageEncoder>> ImageEncoders => this.mimeTypeEncoders;
/// <summary> /// <summary>
/// Registers a new format provider. /// Registers a new format provider.
@ -117,7 +117,7 @@ public class ImageFormatManager
/// </summary> /// </summary>
/// <param name="imageFormat">The image format to register the encoder for.</param> /// <param name="imageFormat">The image format to register the encoder for.</param>
/// <param name="encoder">The encoder to use,</param> /// <param name="encoder">The encoder to use,</param>
public void SetEncoder(IImageFormat imageFormat, ImageEncoder encoder) public void SetEncoder(IImageFormat imageFormat, IImageEncoder encoder)
{ {
Guard.NotNull(imageFormat, nameof(imageFormat)); Guard.NotNull(imageFormat, nameof(imageFormat));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
@ -172,12 +172,12 @@ public class ImageFormatManager
/// For the specified mime type find the encoder. /// For the specified mime type find the encoder.
/// </summary> /// </summary>
/// <param name="format">The format to discover</param> /// <param name="format">The format to discover</param>
/// <returns>The <see cref="ImageEncoder"/> if found otherwise null</returns> /// <returns>The <see cref="IImageEncoder"/> if found otherwise null</returns>
public ImageEncoder FindEncoder(IImageFormat format) public IImageEncoder FindEncoder(IImageFormat format)
{ {
Guard.NotNull(format, nameof(format)); Guard.NotNull(format, nameof(format));
return this.mimeTypeEncoders.TryGetValue(format, out ImageEncoder encoder) return this.mimeTypeEncoders.TryGetValue(format, out IImageEncoder encoder)
? encoder ? encoder
: null; : null;
} }

13
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary> /// <summary>
/// Encoder for writing the data image to a stream in jpeg format. /// Encoder for writing the data image to a stream in jpeg format.
/// </summary> /// </summary>
public sealed class JpegEncoder : ImageEncoder public sealed class JpegEncoder : SynchronousImageEncoder
{ {
/// <summary> /// <summary>
/// Backing field for <see cref="Quality"/>. /// Backing field for <see cref="Quality"/>.
@ -48,16 +48,9 @@ public sealed class JpegEncoder : ImageEncoder
public JpegEncodingColor? ColorType { get; init; } public JpegEncodingColor? ColorType { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
JpegEncoderCore encoder = new(this); JpegEncoderCore encoder = new(this);
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
JpegEncoderCore encoder = new(this);
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

15
src/ImageSharp/Formats/Pbm/PbmEncoder.cs

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// </para> /// </para>
/// The specification of these images is found at <seealso href="http://netpbm.sourceforge.net/doc/pnm.html"/>. /// The specification of these images is found at <seealso href="http://netpbm.sourceforge.net/doc/pnm.html"/>.
/// </summary> /// </summary>
public sealed class PbmEncoder : ImageEncoder public sealed class PbmEncoder : SynchronousImageEncoder
{ {
/// <summary> /// <summary>
/// Gets the encoding of the pixels. /// Gets the encoding of the pixels.
@ -42,21 +42,14 @@ public sealed class PbmEncoder : ImageEncoder
public PbmColorType? ColorType { get; init; } public PbmColorType? ColorType { get; init; }
/// <summary> /// <summary>
/// Gets the Data Type of the pixel components. /// Gets the data type of the pixel components.
/// </summary> /// </summary>
public PbmComponentType? ComponentType { get; init; } public PbmComponentType? ComponentType { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
PbmEncoderCore encoder = new(image.GetConfiguration(), this); PbmEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
PbmEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

14
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -74,19 +74,9 @@ public class PngEncoder : QuantizingImageEncoder
public PngTransparentColorMode TransparentColorMode { get; init; } public PngTransparentColorMode TransparentColorMode { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this); using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
// The introduction of a local variable that refers to an object the implements
// IDisposable means you must use async/await, where the compiler generates the
// state machine and a continuation.
using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
} }
} }

23
src/ImageSharp/Formats/QuantizingImageEncoder.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : SynchronousImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree;
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

94
src/ImageSharp/Formats/SynchronousImageEncoder.cs

@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base class for image encoders.
/// Types that inherit this encoder are required to implement cancellable synchronous encoding operations only.
/// </summary>
public abstract class SynchronousImageEncoder : IImageEncoder
{
/// <inheritdoc/>
public bool SkipMetadata { get; init; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
=> this.EncodeWithSeekableStream(image, stream, default);
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> this.EncodeWithSeekableStreamAsync(image, stream, cancellationToken);
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}" />.
/// </summary>
/// <remarks>
/// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}" /> to encode from.</param>
/// <param name="stream">The <see cref="Stream" /> to encode the image data to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
protected abstract void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = image.GetConfiguration();
if (stream.CanSeek)
{
this.Encode(image, stream, cancellationToken);
}
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
this.Encode(image, stream, cancellationToken);
ms.Position = 0;
ms.CopyTo(stream, configuration.StreamProcessingBufferSize);
}
}
private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = image.GetConfiguration();
if (stream.CanSeek)
{
await DoEncodeAsync(stream).ConfigureAwait(false);
}
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await DoEncodeAsync(ms);
ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)
.ConfigureAwait(false);
}
Task DoEncodeAsync(Stream innerStream)
{
try
{
// TODO: Are synchronous IO writes OK? We avoid reads.
this.Encode(image, innerStream, cancellationToken);
return Task.CompletedTask;
}
catch (OperationCanceledException)
{
return Task.FromCanceled(cancellationToken);
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
}
}

13
src/ImageSharp/Formats/Tga/TgaEncoder.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a targa truevision image. /// Image encoder for writing an image to a stream as a targa truevision image.
/// </summary> /// </summary>
public sealed class TgaEncoder : ImageEncoder public sealed class TgaEncoder : SynchronousImageEncoder
{ {
/// <summary> /// <summary>
/// Gets the number of bits per pixel. /// Gets the number of bits per pixel.
@ -21,16 +21,9 @@ public sealed class TgaEncoder : ImageEncoder
public TgaCompression Compression { get; init; } = TgaCompression.RunLength; public TgaCompression Compression { get; init; } = TgaCompression.RunLength;
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

11
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -40,16 +40,9 @@ public class TiffEncoder : QuantizingImageEncoder
public TiffPredictor? HorizontalPredictor { get; init; } public TiffPredictor? HorizontalPredictor { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
TiffEncoderCore encode = new(this, image.GetMemoryAllocator()); TiffEncoderCore encode = new(this, image.GetMemoryAllocator());
encode.Encode(image, stream); encode.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
TiffEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

13
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream in the Webp format. /// Image encoder for writing an image to a stream in the Webp format.
/// </summary> /// </summary>
public sealed class WebpEncoder : ImageEncoder public sealed class WebpEncoder : SynchronousImageEncoder
{ {
/// <summary> /// <summary>
/// Gets the webp file format used. Either lossless or lossy. /// Gets the webp file format used. Either lossless or lossy.
@ -80,16 +80,9 @@ public sealed class WebpEncoder : ImageEncoder
public int NearLosslessQuality { get; init; } = 100; public int NearLosslessQuality { get; init; } = 100;
/// <inheritdoc/> /// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
WebpEncoderCore encoder = new(this, image.GetMemoryAllocator()); WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

8
src/ImageSharp/Image.cs

@ -101,7 +101,7 @@ public abstract partial class Image : IImage, IConfigurationProvider
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream or encoder is null.</exception> /// <exception cref="ArgumentNullException">Thrown if the stream or encoder is null.</exception>
public void Save(Stream stream, ImageEncoder encoder) public void Save(Stream stream, IImageEncoder encoder)
{ {
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
@ -118,7 +118,7 @@ public abstract partial class Image : IImage, IConfigurationProvider
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="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> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task SaveAsync(Stream stream, ImageEncoder encoder, CancellationToken cancellationToken = default) public Task SaveAsync(Stream stream, IImageEncoder encoder, CancellationToken cancellationToken = default)
{ {
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
@ -189,11 +189,11 @@ public abstract partial class Image : IImage, IConfigurationProvider
private class EncodeVisitor : IImageVisitor, IImageVisitorAsync private class EncodeVisitor : IImageVisitor, IImageVisitorAsync
{ {
private readonly ImageEncoder encoder; private readonly IImageEncoder encoder;
private readonly Stream stream; private readonly Stream stream;
public EncodeVisitor(ImageEncoder encoder, Stream stream) public EncodeVisitor(IImageEncoder encoder, Stream stream)
{ {
this.encoder = encoder; this.encoder = encoder;
this.stream = stream; this.stream = stream;

18
src/ImageSharp/ImageExtensions.cs

@ -43,14 +43,12 @@ public static partial class ImageExtensions
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="ArgumentNullException">The path is null.</exception> /// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="ArgumentNullException">The encoder is null.</exception> /// <exception cref="ArgumentNullException">The encoder is null.</exception>
public static void Save(this Image source, string path, ImageEncoder encoder) public static void Save(this Image source, string path, IImageEncoder encoder)
{ {
Guard.NotNull(path, nameof(path)); Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) using Stream fs = source.GetConfiguration().FileSystem.Create(path);
{ source.Save(fs, encoder);
source.Save(fs, encoder);
}
} }
/// <summary> /// <summary>
@ -66,7 +64,7 @@ public static partial class ImageExtensions
public static async Task SaveAsync( public static async Task SaveAsync(
this Image source, this Image source,
string path, string path,
ImageEncoder encoder, IImageEncoder encoder,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
Guard.NotNull(path, nameof(path)); Guard.NotNull(path, nameof(path));
@ -96,14 +94,14 @@ public static partial class ImageExtensions
throw new NotSupportedException("Cannot write to the stream."); throw new NotSupportedException("Cannot write to the stream.");
} }
ImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, ImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders)
{ {
sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine);
} }
@ -140,14 +138,14 @@ public static partial class ImageExtensions
throw new NotSupportedException("Cannot write to the stream."); throw new NotSupportedException("Cannot write to the stream.");
} }
ImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format); IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.FindEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, ImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders)
{ {
sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine);
} }

10
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -56,7 +56,7 @@ public class ImageFormatManagerTests
[Fact] [Fact]
public void RegisterNullMimeTypeEncoder() public void RegisterNullMimeTypeEncoder()
{ {
Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(null, new Mock<ImageEncoder>().Object)); Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(null, new Mock<IImageEncoder>().Object));
Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(BmpFormat.Instance, null)); Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(BmpFormat.Instance, null));
Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(null, null)); Assert.Throws<ArgumentNullException>(() => this.DefaultFormatsManager.SetEncoder(null, null));
} }
@ -72,14 +72,14 @@ public class ImageFormatManagerTests
[Fact] [Fact]
public void RegisterMimeTypeEncoderReplacesLast() public void RegisterMimeTypeEncoderReplacesLast()
{ {
ImageEncoder encoder1 = new Mock<ImageEncoder>().Object; IImageEncoder encoder1 = new Mock<IImageEncoder>().Object;
this.FormatsManagerEmpty.SetEncoder(TestFormat.GlobalTestFormat, encoder1); this.FormatsManagerEmpty.SetEncoder(TestFormat.GlobalTestFormat, encoder1);
ImageEncoder found = this.FormatsManagerEmpty.FindEncoder(TestFormat.GlobalTestFormat); IImageEncoder found = this.FormatsManagerEmpty.FindEncoder(TestFormat.GlobalTestFormat);
Assert.Equal(encoder1, found); Assert.Equal(encoder1, found);
ImageEncoder encoder2 = new Mock<ImageEncoder>().Object; IImageEncoder encoder2 = new Mock<IImageEncoder>().Object;
this.FormatsManagerEmpty.SetEncoder(TestFormat.GlobalTestFormat, encoder2); this.FormatsManagerEmpty.SetEncoder(TestFormat.GlobalTestFormat, encoder2);
ImageEncoder found2 = this.FormatsManagerEmpty.FindEncoder(TestFormat.GlobalTestFormat); IImageEncoder found2 = this.FormatsManagerEmpty.FindEncoder(TestFormat.GlobalTestFormat);
Assert.Equal(encoder2, found2); Assert.Equal(encoder2, found2);
Assert.NotEqual(found, found2); Assert.NotEqual(found, found2);
} }

8
tests/ImageSharp.Tests/Image/ImageSaveTests.cs

@ -16,8 +16,8 @@ public class ImageSaveTests : IDisposable
{ {
private readonly Image<Rgba32> image; private readonly Image<Rgba32> image;
private readonly Mock<IFileSystem> fileSystem; private readonly Mock<IFileSystem> fileSystem;
private readonly Mock<ImageEncoder> encoder; private readonly Mock<IImageEncoder> encoder;
private readonly Mock<ImageEncoder> encoderNotInFormat; private readonly Mock<IImageEncoder> encoderNotInFormat;
private IImageFormatDetector localMimeTypeDetector; private IImageFormatDetector localMimeTypeDetector;
private Mock<IImageFormat> localImageFormat; private Mock<IImageFormat> localImageFormat;
@ -27,9 +27,9 @@ public class ImageSaveTests : IDisposable
this.localImageFormat.Setup(x => x.FileExtensions).Returns(new[] { "png" }); this.localImageFormat.Setup(x => x.FileExtensions).Returns(new[] { "png" });
this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormat.Object); this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormat.Object);
this.encoder = new Mock<ImageEncoder>(); this.encoder = new Mock<IImageEncoder>();
this.encoderNotInFormat = new Mock<ImageEncoder>(); this.encoderNotInFormat = new Mock<IImageEncoder>();
this.fileSystem = new Mock<IFileSystem>(); this.fileSystem = new Mock<IFileSystem>();
var config = new Configuration var config = new Configuration

2
tests/ImageSharp.Tests/Image/ImageTests.Save.cs

@ -68,7 +68,7 @@ public partial class ImageTests
{ {
using var image = new Image<Rgba32>(5, 5); using var image = new Image<Rgba32>(5, 5);
image.Dispose(); image.Dispose();
ImageEncoder encoder = Mock.Of<ImageEncoder>(); IImageEncoder encoder = Mock.Of<IImageEncoder>();
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
Assert.Throws<ObjectDisposedException>(() => image.Save(stream, encoder)); Assert.Throws<ObjectDisposedException>(() => image.Save(stream, encoder));

4
tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs

@ -102,7 +102,7 @@ public partial class ImageTests
{ {
var image = new Image<Rgba32>(5, 5); var image = new Image<Rgba32>(5, 5);
image.Dispose(); image.Dispose();
ImageEncoder encoder = Mock.Of<ImageEncoder>(); IImageEncoder encoder = Mock.Of<IImageEncoder>();
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await image.SaveAsync(stream, encoder)); await Assert.ThrowsAsync<ObjectDisposedException>(async () => await image.SaveAsync(stream, encoder));
@ -120,7 +120,7 @@ public partial class ImageTests
{ {
using (var image = new Image<Rgba32>(5, 5)) using (var image = new Image<Rgba32>(5, 5))
{ {
ImageEncoder encoder = image.DetectEncoder(filename); IImageEncoder encoder = image.DetectEncoder(filename);
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
var asyncStream = new AsyncStreamWrapper(stream, () => false); var asyncStream = new AsyncStreamWrapper(stream, () => false);

2
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -328,7 +328,7 @@ public partial class ImageTests
public void KnownExtension_ReturnsEncoder() public void KnownExtension_ReturnsEncoder()
{ {
using var image = new Image<L8>(1, 1); using var image = new Image<L8>(1, 1);
ImageEncoder encoder = image.DetectEncoder("dummy.png"); IImageEncoder encoder = image.DetectEncoder("dummy.png");
Assert.NotNull(encoder); Assert.NotNull(encoder);
Assert.IsType<PngEncoder>(encoder); Assert.IsType<PngEncoder>(encoder);
} }

2
tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs

@ -55,7 +55,7 @@ public class LargeImageIntegrationTests
Configuration configuration = Configuration.Default.Clone(); Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true; configuration.PreferContiguousImageBuffers = true;
ImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder( IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder(
configuration.ImageFormatsManager.FindFormatByFileExtension(formatInner)); configuration.ImageFormatsManager.FindFormatByFileExtension(formatInner));
string dir = TestEnvironment.CreateOutputDirectory(".Temp"); string dir = TestEnvironment.CreateOutputDirectory(".Temp");
string path = Path.Combine(dir, $"{Guid.NewGuid()}.{formatInner}"); string path = Path.Combine(dir, $"{Guid.NewGuid()}.{formatInner}");

2
tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs

@ -242,7 +242,7 @@ public class XmpProfileTests
return profile; return profile;
} }
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, ImageEncoder encoder) private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, IImageEncoder encoder)
{ {
using (var memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {

12
tests/ImageSharp.Tests/TestFormat.cs

@ -233,7 +233,7 @@ public class TestFormat : IConfigurationModule, IImageFormat
public DecoderOptions GeneralOptions { get; init; } = DecoderOptions.Default; public DecoderOptions GeneralOptions { get; init; } = DecoderOptions.Default;
} }
public class TestEncoder : ImageEncoder public class TestEncoder : IImageEncoder
{ {
private readonly TestFormat testFormat; private readonly TestFormat testFormat;
@ -243,13 +243,17 @@ public class TestFormat : IConfigurationModule, IImageFormat
public IEnumerable<string> FileExtensions => this.testFormat.SupportedExtensions; public IEnumerable<string> FileExtensions => this.testFormat.SupportedExtensions;
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) public bool SkipMetadata { get; init; }
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
// TODO record this happened so we can verify it. // TODO record this happened so we can verify it.
} }
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
=> Task.CompletedTask; // TODO record this happened so we can verify it. where TPixel : unmanaged, IPixel<TPixel>
=> Task.CompletedTask; // TODO record this happened so we can verify it.
} }
public struct TestPixelForAgnosticDecode : IPixel<TestPixelForAgnosticDecode> public struct TestPixelForAgnosticDecode : IPixel<TestPixelForAgnosticDecode>

4
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -158,7 +158,7 @@ public class ImagingTestCaseUtility
public string SaveTestOutputFile( public string SaveTestOutputFile(
Image image, Image image,
string extension = null, string extension = null,
ImageEncoder encoder = null, IImageEncoder encoder = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true) bool appendSourceFileOrDescription = true)
@ -203,7 +203,7 @@ public class ImagingTestCaseUtility
public string[] SaveTestOutputFileMultiFrame<TPixel>( public string[] SaveTestOutputFileMultiFrame<TPixel>(
Image<TPixel> image, Image<TPixel> image,
string extension = "png", string extension = "png",
ImageEncoder encoder = null, IImageEncoder encoder = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>

32
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -13,38 +12,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
/// </summary> /// </summary>
public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder
{ {
/// <summary> /// <inheritdoc/>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{ {
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator; MemoryAllocator allocator = configuration.MemoryAllocator;
using PngEncoderCore encoder = new(allocator, configuration, this); using PngEncoderCore encoder = new(allocator, configuration, this);
encoder.Encode(image, stream); encoder.Encode(image, stream, cancellationToken);
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public override async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator;
// The introduction of a local variable that refers to an object the implements
// IDisposable means you must use async/await, where the compiler generates the
// state machine and a continuation.
using PngEncoderCore encoder = new(allocator, configuration, this);
await encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

11
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs

@ -3,10 +3,11 @@
using System.Drawing.Imaging; using System.Drawing.Imaging;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
public class SystemDrawingReferenceEncoder : ImageEncoder public class SystemDrawingReferenceEncoder : IImageEncoder
{ {
private readonly ImageFormat imageFormat; private readonly ImageFormat imageFormat;
@ -17,13 +18,17 @@ public class SystemDrawingReferenceEncoder : ImageEncoder
public static SystemDrawingReferenceEncoder Bmp { get; } = new SystemDrawingReferenceEncoder(ImageFormat.Bmp); public static SystemDrawingReferenceEncoder Bmp { get; } = new SystemDrawingReferenceEncoder(ImageFormat.Bmp);
public override void Encode<TPixel>(Image<TPixel> image, Stream stream) public bool SkipMetadata { get; init; }
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
using System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image); using System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image);
sdBitmap.Save(stream, this.imageFormat); sdBitmap.Save(stream, this.imageFormat);
} }
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
using (System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) using (System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image))
{ {

8
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -26,7 +26,7 @@ public static partial class TestEnvironment
return Configuration.ImageFormatsManager.FindDecoder(format); return Configuration.ImageFormatsManager.FindDecoder(format);
} }
internal static ImageEncoder GetReferenceEncoder(string filePath) internal static IImageEncoder GetReferenceEncoder(string filePath)
{ {
IImageFormat format = GetImageFormat(filePath); IImageFormat format = GetImageFormat(filePath);
return Configuration.ImageFormatsManager.FindEncoder(format); return Configuration.ImageFormatsManager.FindEncoder(format);
@ -43,7 +43,7 @@ public static partial class TestEnvironment
this Configuration cfg, this Configuration cfg,
IImageFormat imageFormat, IImageFormat imageFormat,
IImageDecoder decoder, IImageDecoder decoder,
ImageEncoder encoder, IImageEncoder encoder,
IImageFormatDetector detector) IImageFormatDetector detector)
{ {
cfg.ImageFormatsManager.SetDecoder(imageFormat, decoder); cfg.ImageFormatsManager.SetDecoder(imageFormat, decoder);
@ -61,8 +61,8 @@ public static partial class TestEnvironment
new WebpConfigurationModule(), new WebpConfigurationModule(),
new TiffConfigurationModule()); new TiffConfigurationModule());
ImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
ImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
// Magick codecs should work on all platforms // Magick codecs should work on all platforms
cfg.ConfigureCodecs( cfg.ConfigureCodecs(

10
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -30,7 +30,7 @@ public static class TestImageExtensions
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true, bool appendSourceFileOrDescription = true,
ImageEncoder encoder = null) IImageEncoder encoder = null)
=> image.DebugSave( => image.DebugSave(
provider, provider,
(object)testOutputDetails, (object)testOutputDetails,
@ -57,7 +57,7 @@ public static class TestImageExtensions
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true, bool appendSourceFileOrDescription = true,
ImageEncoder encoder = null) IImageEncoder encoder = null)
{ {
if (TestEnvironment.RunsWithCodeCoverage) if (TestEnvironment.RunsWithCodeCoverage)
{ {
@ -77,7 +77,7 @@ public static class TestImageExtensions
public static void DebugSave( public static void DebugSave(
this Image image, this Image image,
ITestImageProvider provider, ITestImageProvider provider,
ImageEncoder encoder, IImageEncoder encoder,
FormattableString testOutputDetails, FormattableString testOutputDetails,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true)
=> image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName); => image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName);
@ -93,7 +93,7 @@ public static class TestImageExtensions
public static void DebugSave( public static void DebugSave(
this Image image, this Image image,
ITestImageProvider provider, ITestImageProvider provider,
ImageEncoder encoder, IImageEncoder encoder,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true)
=> provider.Utility.SaveTestOutputFile( => provider.Utility.SaveTestOutputFile(
@ -664,7 +664,7 @@ public static class TestImageExtensions
ITestImageProvider provider, ITestImageProvider provider,
string extension, string extension,
object testOutputDetails, object testOutputDetails,
ImageEncoder encoder, IImageEncoder encoder,
ImageComparer customComparer = null, ImageComparer customComparer = null,
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
string referenceImageExtension = null, string referenceImageExtension = null,

4
tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs

@ -62,7 +62,7 @@ public class TestEnvironmentTests
return; return;
} }
ImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName);
Assert.IsType(expectedEncoderType, encoder); Assert.IsType(expectedEncoderType, encoder);
} }
@ -96,7 +96,7 @@ public class TestEnvironmentTests
return; return;
} }
ImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName);
Assert.IsType(expectedEncoderType, encoder); Assert.IsType(expectedEncoderType, encoder);
} }

Loading…
Cancel
Save