Browse Source

Merge pull request #2269 from SixLabors/js/encoder-normalization

Normalize and cleanup encoders
pull/2237/merge
James Jackson-South 3 years ago
committed by GitHub
parent
commit
b40482642c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 28
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  3. 60
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 30
      src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
  5. 31
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  6. 32
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  7. 27
      src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs
  8. 4
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  9. 43
      src/ImageSharp/Formats/ImageEncoder.cs
  10. 6
      src/ImageSharp/Formats/ImageEncoderUtilities.cs
  11. 31
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  12. 53
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  13. 41
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  14. 25
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  15. 20
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  16. 77
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  17. 111
      src/ImageSharp/Formats/Png/PngEncoder.cs
  18. 315
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  19. 68
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  20. 214
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  21. 20
      src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs
  22. 21
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  23. 12
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  24. 46
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  25. 61
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  26. 29
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  27. 153
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  28. 3
      src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
  29. 34
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  30. 12
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  31. 79
      src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs
  32. 15
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  33. 20
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  34. 92
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  35. 36
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  36. 10
      src/ImageSharp/ImageExtensions.Internal.cs
  37. 6
      src/ImageSharp/ImageFrameCollection.cs
  38. 17
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  39. 43
      src/ImageSharp/Image{TPixel}.cs
  40. 56
      src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs
  41. 7
      src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs
  42. 13
      src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs
  43. 38
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  44. 221
      tests/ImageSharp.Tests/Drawing/DrawImageTests.cs
  45. 166
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  46. 90
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  47. 23
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs
  48. 142
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
  49. 194
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  50. 100
      tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs
  51. 50
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs

15
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -57,6 +57,9 @@ internal static class AotCompilerTools
/// If you are getting the above error, you need to call this method, which will pre-seed the AoT compiler with the
/// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!!
/// </remarks>
/// <exception cref="InvalidOperationException">
/// This method is used for AOT code generation only. Do not call it at runtime.
/// </exception>
[Preserve]
private static void SeedPixelFormats()
{
@ -487,8 +490,10 @@ internal static class AotCompilerTools
private static void AotCompilePixelSamplingStrategys<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(DefaultPixelSamplingStrategy).EnumeratePixelRegions<TPixel>(default);
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions<TPixel>(default);
default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(Image<TPixel>));
default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(Image<TPixel>));
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
}
/// <summary>
@ -513,13 +518,13 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
where TDither : struct, IDither
{
var octree = default(OctreeQuantizer<TPixel>);
OctreeQuantizer<TPixel> octree = default;
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, default);
var palette = default(PaletteQuantizer<TPixel>);
PaletteQuantizer<TPixel> palette = default;
default(TDither).ApplyQuantizationDither<PaletteQuantizer<TPixel>, TPixel>(ref palette, default, default, default);
var wu = default(WuQuantizer<TPixel>);
WuQuantizer<TPixel> wu = default;
default(TDither).ApplyQuantizationDither<WuQuantizer<TPixel>, TPixel>(ref wu, default, default, default);
default(TDither).ApplyPaletteDither<PaletteDitherProcessor<TPixel>.DitherProcessor, TPixel>(default, default, default);
}

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

@ -2,48 +2,38 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary>
/// Image encoder for writing an image to a stream as a Windows bitmap.
/// </summary>
public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions
public sealed class BmpEncoder : QuantizingImageEncoder
{
/// <summary>
/// Gets or sets the number of bits per pixel.
/// Gets the number of bits per pixel.
/// </summary>
public BmpBitsPerPixel? BitsPerPixel { get; set; }
public BmpBitsPerPixel? BitsPerPixel { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the encoder should support transparency.
/// Gets a value indicating whether the encoder should support transparency.
/// Note: Transparency support only works together with 32 bits per pixel. This option will
/// change the default behavior of the encoder of writing a bitmap version 3 info header with no compression.
/// Instead a bitmap version 4 info header will be written with the BITFIELDS compression.
/// </summary>
public bool SupportTransparency { get; set; }
/// <summary>
/// Gets or sets the quantizer for reducing the color count for 8-Bit images.
/// Defaults to Wu Quantizer.
/// </summary>
public IQuantizer Quantizer { get; set; }
public bool SupportTransparency { get; init; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator());
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator());
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

60
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
@ -92,17 +91,23 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// The pixel sampling strategy for quantization.
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary>
/// <param name="options">The encoder options.</param>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocator)
public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
}
/// <summary>
@ -159,7 +164,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
this.WriteImage(stream, image);
WriteColorProfile(stream, iccProfileData, buffer);
stream.Flush();
@ -311,10 +316,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void WriteImage<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = image.PixelBuffer;
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel)
{
case BmpBitsPerPixel.Pixel32:
@ -433,8 +438,8 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
bool isL8 = typeof(TPixel) == typeof(L8);
@ -456,13 +461,15 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
private void Write8BitColor<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
@ -486,7 +493,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
// Create a color palette with 256 different gray values.
@ -503,7 +510,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
}
stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.PixelBuffer;
Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer();
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
@ -523,14 +530,17 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write4BitPixelData<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 16
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -567,14 +577,17 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write2BitPixelData<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 4
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -620,14 +633,17 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write1BitPixelData<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 2
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();

30
src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs

@ -1,30 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary>
/// Configuration options for use during bmp encoding.
/// </summary>
internal interface IBmpEncoderOptions
{
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
BmpBitsPerPixel? BitsPerPixel { get; }
/// <summary>
/// Gets a value indicating whether the encoder should support transparency.
/// Note: Transparency support only works together with 32 bits per pixel. This option will
/// change the default behavior of the encoder of writing a bitmap version 3 info header with no compression.
/// Instead a bitmap version 4 info header will be written with the BITFIELDS compression.
/// </summary>
bool SupportTransparency { get; }
/// <summary>
/// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images.
/// </summary>
IQuantizer Quantizer { get; }
}

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

@ -2,47 +2,30 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions
public sealed class GifEncoder : QuantizingImageEncoder
{
/// <summary>
/// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/>
/// Gets the color table mode: Global or local.
/// </summary>
public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
/// <summary>
/// Gets or sets the color table mode: Global or local.
/// </summary>
public GifColorTableMode? ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the <see cref="IPixelSamplingStrategy"/> used for quantization
/// when building a global color table in case of <see cref="GifColorTableMode.Global"/>.
/// </summary>
public IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; set; } = new DefaultPixelSamplingStrategy();
public GifColorTableMode? ColorTableMode { get; init; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new GifEncoderCore(image.GetConfiguration(), this);
GifEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new GifEncoderCore(image.GetConfiguration(), this);
GifEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

32
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -33,6 +33,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary>
private readonly byte[] buffer = new byte[20];
/// <summary>
/// Whether to skip metadata during encode.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
@ -57,14 +62,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The options for the encoder.</param>
public GifEncoderCore(Configuration configuration, IGifEncoderOptions options)
/// <param name="encoder">The encoder with options.</param>
public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
this.pixelSamplingStrategy = options.GlobalPixelSamplingStrategy;
this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
}
/// <summary>
@ -97,7 +103,8 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
else
{
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds());
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
}
@ -116,12 +123,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteColorTable(quantized, stream);
}
// Write the comments.
this.WriteComments(gifMetadata, stream);
if (!this.skipMetadata)
{
// Write the comments.
this.WriteComments(gifMetadata, stream);
// Write application extensions.
XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
// Write application extensions.
XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
if (useGlobalTable)
{

27
src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs

@ -1,27 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// The configuration options used for encoding gifs.
/// </summary>
internal interface IGifEncoderOptions
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer Quantizer { get; }
/// <summary>
/// Gets the color table mode: Global or local.
/// </summary>
GifColorTableMode? ColorTableMode { get; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building a global color table.
/// </summary>
IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; }
}

4
src/ImageSharp/Formats/ImageDecoderUtilities.cs

@ -58,7 +58,7 @@ internal static class ImageDecoderUtilities
Stream stream,
CancellationToken cancellationToken)
{
using var bufferedReadStream = new BufferedReadStream(configuration, stream);
using BufferedReadStream bufferedReadStream = new(configuration, stream);
try
{
@ -86,7 +86,7 @@ internal static class ImageDecoderUtilities
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var bufferedReadStream = new BufferedReadStream(configuration, stream);
using BufferedReadStream bufferedReadStream = new(configuration, stream);
try
{

43
src/ImageSharp/Formats/ImageEncoder.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// The base class for all image encoders.
/// </summary>
public abstract class ImageEncoder : IImageEncoder
{
/// <summary>
/// Gets a value indicating whether to ignore decoded metadata when encoding.
/// </summary>
public bool SkipMetadata { get; init; }
/// <inheritdoc/>
public abstract void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>;
/// <inheritdoc/>
public abstract Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
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();
}

6
src/ImageSharp/Formats/ImageEncoderUtilities.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
@ -22,11 +22,11 @@ internal static class ImageEncoderUtilities
}
else
{
using var ms = new MemoryStream();
using MemoryStream ms = new();
await DoEncodeAsync(ms);
ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)
.ConfigureAwait(false);
.ConfigureAwait(false);
}
Task DoEncodeAsync(Stream innerStream)

31
src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs

@ -1,31 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary>
/// Encoder for writing the data image to a stream in jpeg format.
/// </summary>
internal interface IJpegEncoderOptions
{
/// <summary>
/// Gets or sets the quality, that will be used to encode the image. Quality
/// index must be between 0 and 100 (compression from max to min).
/// Defaults to <value>75</value>.
/// </summary>
public int? Quality { get; set; }
/// <summary>
/// Gets or sets the component encoding mode.
/// </summary>
/// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks>
public bool? Interleaved { get; set; }
/// <summary>
/// Gets or sets jpeg color for encoding.
/// </summary>
public JpegEncodingColor? ColorType { get; set; }
}

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

@ -1,25 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary>
/// Encoder for writing the data image to a stream in jpeg format.
/// </summary>
public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions
public sealed class JpegEncoder : ImageEncoder
{
/// <summary>
/// Backing field for <see cref="Quality"/>.
/// </summary>
private int? quality;
/// <inheritdoc/>
/// <summary>
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min).
/// Defaults to <value>75</value>.
/// </summary>
/// <exception cref="ArgumentException">Quality factor must be in [1..100] range.</exception>
public int? Quality
{
get => this.quality;
set
init
{
if (value is < 1 or > 100)
{
@ -30,37 +33,31 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions
}
}
/// <inheritdoc/>
public bool? Interleaved { get; set; }
/// <inheritdoc/>
public JpegEncodingColor? ColorType { get; set; }
/// <summary>
/// Gets the component encoding mode.
/// </summary>
/// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks>
public bool? Interleaved { get; init; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// Gets the jpeg color for encoding.
/// </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 void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public JpegEncodingColor? ColorType { get; init; }
/// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new JpegEncoderCore(this);
JpegEncoderCore encoder = new(this);
encoder.Encode(image, stream);
}
/// <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 Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
/// <inheritdoc/>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new JpegEncoderCore(this);
JpegEncoderCore encoder = new(this);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

41
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -29,7 +29,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// </summary>
private readonly byte[] buffer = new byte[20];
private readonly IJpegEncoderOptions options;
private readonly JpegEncoder encoder;
/// <summary>
/// The output stream. All attempted writes after the first error become no-ops.
@ -39,9 +39,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Initializes a new instance of the <see cref="JpegEncoderCore"/> class.
/// </summary>
/// <param name="options">The options.</param>
public JpegEncoderCore(IJpegEncoderOptions options)
=> this.options = options;
/// <param name="encoder">The parent encoder.</param>
public JpegEncoderCore(JpegEncoder encoder)
=> this.encoder = encoder;
public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4];
@ -71,8 +71,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata);
bool interleaved = this.options.Interleaved ?? jpegMetadata.Interleaved ?? true;
using var frame = new JpegFrame(image, frameConfig, interleaved);
bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved ?? true;
using JpegFrame frame = new(image, frameConfig, interleaved);
// Write the Start Of Image marker.
this.WriteStartOfImage();
@ -96,14 +96,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
this.WriteStartOfFrame(image.Width, image.Height, frameConfig);
// Write the Huffman tables.
var scanEncoder = new HuffmanScanEncoder(frame.BlocksPerMcu, stream);
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder);
// Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.options.Quality, jpegMetadata);
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata);
// Write scans with actual pixel data
using var spectralConverter = new SpectralConverter<TPixel>(frame, image, this.QuantizationTables);
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken);
// Write the End Of Image marker.
@ -172,6 +172,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the Define Huffman Table marker and tables.
/// </summary>
/// <param name="tableConfigs">The table configuration.</param>
/// <param name="scanEncoder">The scan encoder.</param>
/// <exception cref="ArgumentNullException"><paramref name="tableConfigs"/> is <see langword="null"/>.</exception>
private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder)
{
if (tableConfigs is null)
@ -203,6 +206,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the APP14 marker to indicate the image is in RGB color space.
/// </summary>
/// <param name="colorTransform">The color transform byte.</param>
private void WriteApp14Marker(byte colorTransform)
{
this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length);
@ -498,6 +502,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the Start Of Frame (Baseline) marker.
/// </summary>
/// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param>
/// <param name="frame">The frame configuration.</param>
private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame)
{
JpegComponentConfig[] components = frame.Components;
@ -536,6 +543,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components)
{
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
@ -588,7 +596,18 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes scans for given config.
/// </summary>
private void WriteHuffmanScans<TPixel>(JpegFrame frame, JpegFrameConfig frameConfig, SpectralConverter<TPixel> spectralConverter, HuffmanScanEncoder encoder, CancellationToken cancellationToken)
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="frame">The current frame.</param>
/// <param name="frameConfig">The frame configuration.</param>
/// <param name="spectralConverter">The spectral converter.</param>
/// <param name="encoder">The scan encoder.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void WriteHuffmanScans<TPixel>(
JpegFrame frame,
JpegFrameConfig frameConfig,
SpectralConverter<TPixel> spectralConverter,
HuffmanScanEncoder encoder,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Components.Length == 1)
@ -696,7 +715,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
private JpegFrameConfig GetFrameConfig(JpegMetadata metadata)
{
JpegEncodingColor color = this.options.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420;
JpegEncodingColor color = this.encoder.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420;
JpegFrameConfig frameConfig = Array.Find(
FrameConfigs,
cfg => cfg.EncodingColor == color);

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

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm;
@ -30,36 +29,34 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// </para>
/// The specification of these images is found at <seealso href="http://netpbm.sourceforge.net/doc/pnm.html"/>.
/// </summary>
public sealed class PbmEncoder : IImageEncoder, IPbmEncoderOptions
public sealed class PbmEncoder : ImageEncoder
{
/// <summary>
/// Gets or sets the Encoding of the pixels.
/// Gets the encoding of the pixels.
/// </summary>
public PbmEncoding? Encoding { get; set; }
public PbmEncoding? Encoding { get; init; }
/// <summary>
/// Gets or sets the Color type of the resulting image.
/// Gets the Color type of the resulting image.
/// </summary>
public PbmColorType? ColorType { get; set; }
public PbmColorType? ColorType { get; init; }
/// <summary>
/// Gets or sets the data type of the pixels components.
/// Gets the Data Type of the pixel components.
/// </summary>
public PbmComponentType? ComponentType { get; set; }
public PbmComponentType? ComponentType { get; init; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new PbmEncoderCore(image.GetConfiguration(), this);
PbmEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new PbmEncoderCore(image.GetConfiguration(), this);
PbmEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

20
src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs

@ -22,9 +22,9 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
private Configuration configuration;
/// <summary>
/// The encoder options.
/// The encoder with options.
/// </summary>
private readonly IPbmEncoderOptions options;
private readonly PbmEncoder encoder;
/// <summary>
/// The encoding for the pixels.
@ -45,11 +45,11 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
/// Initializes a new instance of the <see cref="PbmEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="options">The encoder options.</param>
public PbmEncoderCore(Configuration configuration, IPbmEncoderOptions options)
/// <param name="encoder">The encoder with options.</param>
public PbmEncoderCore(Configuration configuration, PbmEncoder encoder)
{
this.configuration = configuration;
this.options = options;
this.encoder = encoder;
}
/// <summary>
@ -65,7 +65,7 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.DeduceOptions(image);
this.SanitizeAndSetEncoderOptions(image);
byte signature = this.DeduceSignature();
this.WriteHeader(stream, signature, image.Size());
@ -75,16 +75,16 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
stream.Flush();
}
private void DeduceOptions<TPixel>(Image<TPixel> image)
private void SanitizeAndSetEncoderOptions<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
this.configuration = image.GetConfiguration();
PbmMetadata metadata = image.Metadata.GetPbmMetadata();
this.encoding = this.options.Encoding ?? metadata.Encoding;
this.colorType = this.options.ColorType ?? metadata.ColorType;
this.encoding = this.encoder.Encoding ?? metadata.Encoding;
this.colorType = this.encoder.ColorType ?? metadata.ColorType;
if (this.colorType != PbmColorType.BlackAndWhite)
{
this.componentType = this.options.ComponentType ?? metadata.ComponentType;
this.componentType = this.encoder.ComponentType ?? metadata.ComponentType;
}
else
{

77
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -1,77 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// The options available for manipulating the encoder pipeline.
/// </summary>
internal interface IPngEncoderOptions
{
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary>
PngBitDepth? BitDepth { get; }
/// <summary>
/// Gets the color type.
/// </summary>
PngColorType? ColorType { get; }
/// <summary>
/// Gets the filter method.
/// </summary>
PngFilterMethod? FilterMethod { get; }
/// <summary>
/// Gets the compression level 1-9.
/// <remarks>Defaults to <see cref="PngCompressionLevel.DefaultCompression"/>.</remarks>
/// </summary>
PngCompressionLevel CompressionLevel { get; }
/// <summary>
/// Gets the threshold of characters in text metadata, when compression should be used.
/// </summary>
int TextCompressionThreshold { get; }
/// <summary>
/// Gets the gamma value, that will be written the image.
/// </summary>
/// <value>The gamma value of the image.</value>
float? Gamma { get; }
/// <summary>
/// Gets the quantizer for reducing the color count.
/// </summary>
IQuantizer Quantizer { get; }
/// <summary>
/// Gets the transparency threshold.
/// </summary>
byte Threshold { get; }
/// <summary>
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
PngInterlaceMode? InterlaceMethod { get; }
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being encoded.
/// When set to true, all ancillary chunks will be skipped.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the chunk filter method. This allows to filter ancillary chunks.
/// </summary>
PngChunkFilter? ChunkFilter { get; }
/// <summary>
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
/// should be converted to transparent black, which can yield in better compression in some cases.
/// </summary>
PngTransparentColorMode TransparentColorMode { get; }
}

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

@ -2,84 +2,91 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
public class PngEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
public PngBitDepth? BitDepth { get; set; }
/// <inheritdoc/>
public PngColorType? ColorType { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder() =>
/// <inheritdoc/>
public PngFilterMethod? FilterMethod { get; set; }
// We set the quantizer to null here to allow the underlying encoder to create a
// quantizer with options appropriate to the encoding bit depth.
this.Quantizer = null;
/// <inheritdoc/>
public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression;
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType" /> values.
/// </summary>
public PngBitDepth? BitDepth { get; init; }
/// <inheritdoc/>
public int TextCompressionThreshold { get; set; } = 1024;
/// <summary>
/// Gets the color type.
/// </summary>
public PngColorType? ColorType { get; init; }
/// <inheritdoc/>
public float? Gamma { get; set; }
/// <summary>
/// Gets the filter method.
/// </summary>
public PngFilterMethod? FilterMethod { get; init; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets the compression level 1-9.
/// <remarks>Defaults to <see cref="PngCompressionLevel.DefaultCompression" />.</remarks>
/// </summary>
public PngCompressionLevel CompressionLevel { get; init; } = PngCompressionLevel.DefaultCompression;
/// <inheritdoc/>
public byte Threshold { get; set; } = byte.MaxValue;
/// <summary>
/// Gets the threshold of characters in text metadata, when compression should be used.
/// </summary>
public int TextCompressionThreshold { get; init; } = 1024;
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <summary>
/// Gets the gamma value, that will be written the image.
/// </summary>
/// <value>The gamma value of the image.</value>
public float? Gamma { get; init; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <summary>
/// Gets the transparency threshold.
/// </summary>
public byte Threshold { get; init; } = byte.MaxValue;
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <summary>
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; init; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
/// <summary>
/// Gets the chunk filter method. This allows to filter ancillary chunks.
/// </summary>
public PngChunkFilter? ChunkFilter { get; init; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
/// should be converted to transparent black, which can yield in better compression in some cases.
/// </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 void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public PngTransparentColorMode TransparentColorMode { get; init; }
/// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this)))
{
encoder.Encode(image, stream);
}
using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
encoder.Encode(image, stream);
}
/// <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 async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
/// <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 (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this)))
{
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
}
using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
}
}

315
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -5,6 +5,7 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -12,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
@ -46,17 +48,42 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private readonly byte[] chunkDataBuffer = new byte[16];
/// <summary>
/// The encoder options
/// The encoder with options
/// </summary>
private readonly PngEncoderOptions options;
private readonly PngEncoder encoder;
/// <summary>
/// The bit depth.
/// The gamma value
/// </summary>
private float? gamma;
/// <summary>
/// The color type.
/// </summary>
private PngColorType colorType;
/// <summary>
/// The number of bits per sample or per palette index (not per pixel).
/// </summary>
private byte bitDepth;
/// <summary>
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
/// The filter method used to prefilter the encoded pixels before compression.
/// </summary>
private PngFilterMethod filterMethod;
/// <summary>
/// Gets the interlace mode.
/// </summary>
private PngInterlaceMode interlaceMode;
/// <summary>
/// The chunk filter method. This allows to filter ancillary chunks.
/// </summary>
private PngChunkFilter chunkFilter;
/// <summary>
/// A value indicating whether to use 16 bit encoding for supported color types.
/// </summary>
private bool use16Bit;
@ -95,12 +122,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="options">The options for influencing the encoder</param>
public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoderOptions options)
/// <param name="encoder">The encoder with options.</param>
public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.options = options;
this.encoder = encoder;
}
/// <summary>
@ -122,16 +149,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear;
bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
if (clearTransparency)
{
clonedImage = image.Clone();
ClearTransparentPixels(clonedImage);
}
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage);
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
stream.Write(PngConstants.HeaderBytes);
@ -171,6 +198,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
where TPixel : unmanaged, IPixel<TPixel> =>
image.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 rgba32 = default;
Rgba32 transparent = Color.Transparent;
for (int y = 0; y < accessor.Height; y++)
@ -189,27 +217,28 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
});
/// <summary>
/// Creates the quantized image and sets calculates and sets the bit depth.
/// Creates the quantized image and calculates and sets the bit depth.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImage<TPixel>(Image<TPixel> image, Image<TPixel> clonedImage)
private IndexedImageFrame<TPixel> CreateQuantizedImageAndUpdateBitDepth<TPixel>(
Image<TPixel> image,
Image<TPixel> clonedImage)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> quantized;
if (this.options.TransparentColorMode == PngTransparentColorMode.Clear)
if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
}
else
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
}
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
@ -223,23 +252,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> rawScanlineSpan = this.currentScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.options.ColorType == PngColorType.Grayscale)
if (this.colorType == PngColorType.Grayscale)
{
if (this.use16Bit)
{
// 16 bit grayscale
using (IMemoryOwner<L16> luminanceBuffer = this.memoryAllocator.Allocate<L16>(rowSpan.Length))
{
Span<L16> luminanceSpan = luminanceBuffer.GetSpan();
ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
using IMemoryOwner<L16> luminanceBuffer = this.memoryAllocator.Allocate<L16>(rowSpan.Length);
Span<L16> luminanceSpan = luminanceBuffer.GetSpan();
ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
// Can't map directly to byte array as it's big-endian.
for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
{
L16 luminance = Unsafe.Add(ref luminanceRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
}
// Can't map directly to byte array as it's big-endian.
for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
{
L16 luminance = Unsafe.Add(ref luminanceRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
}
}
else if (this.bitDepth == 8)
@ -382,7 +409,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel>
{
switch (this.options.ColorType)
switch (this.colorType)
{
case PngColorType.Palette:
@ -413,7 +440,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="attempt">Used for attempting optimized filtering.</param>
private void FilterPixelBytes(ref Span<byte> filter, ref Span<byte> attempt)
{
switch (this.options.FilterMethod)
switch (this.filterMethod)
{
case PngFilterMethod.None:
NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
@ -495,7 +522,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
// Palette images don't compress well with adaptive filtering.
// Nor do images comprising a single row.
if (this.options.ColorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8)
if (this.colorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8)
{
NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
return;
@ -543,10 +570,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
width: this.width,
height: this.height,
bitDepth: this.bitDepth,
colorType: this.options.ColorType.Value,
colorType: this.colorType,
compressionMethod: 0, // None
filterMethod: 0,
interlaceMethod: this.options.InterlaceMethod.Value);
interlaceMethod: this.interlaceMode);
header.WriteTo(this.chunkDataBuffer);
@ -593,7 +620,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
byte alpha = rgba.A;
Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
if (alpha > this.options.Threshold)
if (alpha > this.encoder.Threshold)
{
alpha = byte.MaxValue;
}
@ -619,7 +646,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param>
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
if ((this.chunkFilter & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
{
return;
}
@ -636,7 +663,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param>
private void WriteExifChunk(Stream stream, ImageMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
if ((this.chunkFilter & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
{
return;
}
@ -658,7 +685,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void WriteXmpChunk(Stream stream, ImageMetadata meta)
{
const int iTxtHeaderSize = 5;
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
@ -731,7 +758,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param>
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
@ -754,7 +781,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold
byte[] textBytes = textData.Value.Length > this.encoder.TextCompressionThreshold
? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
@ -768,7 +795,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
keywordBytes.CopyTo(outputBytes);
int bytesWritten = keywordBytes.Length;
outputBytes[bytesWritten++] = 0;
if (textData.Value.Length > this.options.TextCompressionThreshold)
if (textData.Value.Length > this.encoder.TextCompressionThreshold)
{
// Indicate that the text is compressed.
outputBytes[bytesWritten++] = 1;
@ -788,7 +815,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
textBytes.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes);
}
else if (textData.Value.Length > this.options.TextCompressionThreshold)
else if (textData.Value.Length > this.encoder.TextCompressionThreshold)
{
// Write zTXt chunk.
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value));
@ -827,7 +854,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private byte[] GetZlibCompressedBytes(byte[] dataBytes)
{
using MemoryStream memoryStream = new();
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{
deflateStream.Write(dataBytes);
}
@ -842,15 +869,15 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteGammaChunk(Stream stream)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
if ((this.chunkFilter & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
{
return;
}
if (this.options.Gamma > 0)
if (this.gamma > 0)
{
// 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.options.Gamma * 100_000F);
uint gammaValue = (uint)(this.gamma * 100_000F);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), gammaValue);
@ -924,9 +951,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
using (MemoryStream memoryStream = new())
{
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{
if (this.options.InterlaceMethod == PngInterlaceMode.Adam7)
if (this.interlaceMode == PngInterlaceMode.Adam7)
{
if (quantized != null)
{
@ -1192,4 +1219,196 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ref IMemoryOwner<byte> current = ref this.currentScanline;
RuntimeUtility.Swap(ref prev, ref current);
}
/// <summary>
/// Adjusts the options based upon the given metadata.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="encoder">The encoder with options.</param>
/// <param name="pngMetadata">The PNG metadata.</param>
/// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param>
private void SanitizeAndSetEncoderOptions<TPixel>(
PngEncoder encoder,
PngMetadata pngMetadata,
out bool use16Bit,
out int bytesPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// Always take the encoder options over the metadata values.
this.gamma = encoder.Gamma ?? pngMetadata.Gamma;
// Use options, then check metadata, if nothing set there then we suggest
// a sensible default based upon the pixel format.
this.colorType = encoder.ColorType ?? pngMetadata.ColorType ?? SuggestColorType<TPixel>();
if (!encoder.FilterMethod.HasValue)
{
// Specification recommends default filter method None for paletted images and Paeth for others.
if (this.colorType == PngColorType.Palette)
{
this.filterMethod = PngFilterMethod.None;
}
else
{
this.filterMethod = PngFilterMethod.Paeth;
}
}
// Ensure bit depth and color type are a supported combination.
// Bit8 is the only bit depth supported by all color types.
byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth ?? SuggestBitDepth<TPixel>());
byte[] validBitDepths = PngConstants.ColorTypes[this.colorType];
if (Array.IndexOf(validBitDepths, bits) == -1)
{
bits = (byte)PngBitDepth.Bit8;
}
this.bitDepth = bits;
use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod).Value;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
}
/// <summary>
/// Creates the quantized frame.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="encoder">The png encoder.</param>
/// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param>
/// <param name="image">The image.</param>
private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (colorType != PngColorType.Palette)
{
return null;
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
IQuantizer quantizer = encoder.Quantizer
?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
// Create quantized frame returning the palette and set the bit depth.
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration());
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image);
return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
/// <summary>
/// Calculates the bit depth value.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param>
/// <param name="quantizedFrame">The quantized frame.</param>
/// <exception cref="NotSupportedException">Bit depth is not supported or not valid.</exception>
private static byte CalculateBitDepth<TPixel>(
PngColorType colorType,
byte bitDepth,
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (colorType == PngColorType.Palette)
{
byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length), 1, 8);
byte bits = Math.Max(bitDepth, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range.
if (bits == 3)
{
bits = 4;
}
else if (bits is >= 5 and <= 7)
{
bits = 8;
}
bitDepth = bits;
}
if (Array.IndexOf(PngConstants.ColorTypes[colorType], bitDepth) < 0)
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
return bitDepth;
}
/// <summary>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
/// <param name="pngColorType">The color type.</param>
/// <param name="use16Bit">Whether to use 16 bits per component.</param>
/// <returns>Bytes per pixel.</returns>
private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16Bit)
=> pngColorType switch
{
PngColorType.Grayscale => use16Bit ? 2 : 1,
PngColorType.GrayscaleWithAlpha => use16Bit ? 4 : 2,
PngColorType.Palette => 1,
PngColorType.Rgb => use16Bit ? 6 : 3,
// PngColorType.RgbWithAlpha
_ => use16Bit ? 8 : 4,
};
/// <summary>
/// Returns a suggested <see cref="PngColorType"/> for the given <typeparamref name="TPixel"/>
/// This is not exhaustive but covers many common pixel formats.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch
{
Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Bgr24) => PngColorType.Rgb,
Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(L8) => PngColorType.Grayscale,
Type t when t == typeof(L16) => PngColorType.Grayscale,
Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Rgb24) => PngColorType.Rgb,
Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Rgb48) => PngColorType.Rgb,
Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha,
Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha
};
/// <summary>
/// Returns a suggested <see cref="PngBitDepth"/> for the given <typeparamref name="TPixel"/>
/// This is not exhaustive but covers many common pixel formats.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch
{
Type t when t == typeof(A8) => PngBitDepth.Bit8,
Type t when t == typeof(Argb32) => PngBitDepth.Bit8,
Type t when t == typeof(Bgr24) => PngBitDepth.Bit8,
Type t when t == typeof(Bgra32) => PngBitDepth.Bit8,
Type t when t == typeof(L8) => PngBitDepth.Bit8,
Type t when t == typeof(L16) => PngBitDepth.Bit16,
Type t when t == typeof(La16) => PngBitDepth.Bit8,
Type t when t == typeof(La32) => PngBitDepth.Bit16,
Type t when t == typeof(Rgb24) => PngBitDepth.Bit8,
Type t when t == typeof(Rgba32) => PngBitDepth.Bit8,
Type t when t == typeof(Rgb48) => PngBitDepth.Bit16,
Type t when t == typeof(Rgba64) => PngBitDepth.Bit16,
Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8
};
}

68
src/ImageSharp/Formats/Png/PngEncoderOptions.cs

@ -1,68 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// The options structure for the <see cref="PngEncoderCore"/>.
/// </summary>
internal class PngEncoderOptions : IPngEncoderOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderOptions"/> class.
/// </summary>
/// <param name="source">The source.</param>
public PngEncoderOptions(IPngEncoderOptions source)
{
this.BitDepth = source.BitDepth;
this.ColorType = source.ColorType;
this.FilterMethod = source.FilterMethod;
this.CompressionLevel = source.CompressionLevel;
this.TextCompressionThreshold = source.TextCompressionThreshold;
this.Gamma = source.Gamma;
this.Quantizer = source.Quantizer;
this.Threshold = source.Threshold;
this.InterlaceMethod = source.InterlaceMethod;
this.ChunkFilter = source.ChunkFilter;
this.IgnoreMetadata = source.IgnoreMetadata;
this.TransparentColorMode = source.TransparentColorMode;
}
/// <inheritdoc/>
public PngBitDepth? BitDepth { get; set; }
/// <inheritdoc/>
public PngColorType? ColorType { get; set; }
/// <inheritdoc/>
public PngFilterMethod? FilterMethod { get; set; }
/// <inheritdoc/>
public PngCompressionLevel CompressionLevel { get; } = PngCompressionLevel.DefaultCompression;
/// <inheritdoc/>
public int TextCompressionThreshold { get; }
/// <inheritdoc/>
public float? Gamma { get; set; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public byte Threshold { get; }
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
}

214
src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

@ -1,214 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// The helper methods for the PNG encoder options.
/// </summary>
internal static class PngEncoderOptionsHelpers
{
/// <summary>
/// Adjusts the options based upon the given metadata.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="pngMetadata">The PNG metadata.</param>
/// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param>
public static void AdjustOptions<TPixel>(
PngEncoderOptions options,
PngMetadata pngMetadata,
out bool use16Bit,
out int bytesPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// Always take the encoder options over the metadata values.
options.Gamma ??= pngMetadata.Gamma;
// Use options, then check metadata, if nothing set there then we suggest
// a sensible default based upon the pixel format.
options.ColorType ??= pngMetadata.ColorType ?? SuggestColorType<TPixel>();
options.BitDepth ??= pngMetadata.BitDepth ?? SuggestBitDepth<TPixel>();
if (!options.FilterMethod.HasValue)
{
// Specification recommends default filter method None for paletted images and Paeth for others.
if (options.ColorType == PngColorType.Palette)
{
options.FilterMethod = PngFilterMethod.None;
}
else
{
options.FilterMethod = PngFilterMethod.Paeth;
}
}
// Ensure bit depth and color type are a supported combination.
// Bit8 is the only bit depth supported by all color types.
byte bits = (byte)options.BitDepth;
byte[] validBitDepths = PngConstants.ColorTypes[options.ColorType.Value];
if (Array.IndexOf(validBitDepths, bits) == -1)
{
options.BitDepth = PngBitDepth.Bit8;
}
options.InterlaceMethod ??= pngMetadata.InterlaceMethod;
use16Bit = options.BitDepth == PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit);
if (options.IgnoreMetadata)
{
options.ChunkFilter = PngChunkFilter.ExcludeAll;
}
}
/// <summary>
/// Creates the quantized frame.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options,
Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (options.ColorType != PngColorType.Palette)
{
return null;
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (options.Quantizer is null)
{
byte bits = (byte)options.BitDepth;
var maxColors = ColorNumerics.GetColorCountForBitDepth(bits);
options.Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = maxColors });
}
// Create quantized frame returning the palette and set the bit depth.
using (IQuantizer<TPixel> frameQuantizer = options.Quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration()))
{
ImageFrame<TPixel> frame = image.Frames.RootFrame;
return frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}
}
/// <summary>
/// Calculates the bit depth value.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="quantizedFrame">The quantized frame.</param>
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
byte bitDepth;
if (options.ColorType == PngColorType.Palette)
{
byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length), 1, 8);
byte bits = Math.Max((byte)options.BitDepth, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range.
if (bits == 3)
{
bits = 4;
}
else if (bits >= 5 && bits <= 7)
{
bits = 8;
}
bitDepth = bits;
}
else
{
bitDepth = (byte)options.BitDepth;
}
if (Array.IndexOf(PngConstants.ColorTypes[options.ColorType.Value], bitDepth) == -1)
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
return bitDepth;
}
/// <summary>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
/// <returns>Bytes per pixel.</returns>
private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16Bit)
{
return pngColorType switch
{
PngColorType.Grayscale => use16Bit ? 2 : 1,
PngColorType.GrayscaleWithAlpha => use16Bit ? 4 : 2,
PngColorType.Palette => 1,
PngColorType.Rgb => use16Bit ? 6 : 3,
// PngColorType.RgbWithAlpha
_ => use16Bit ? 8 : 4,
};
}
/// <summary>
/// Returns a suggested <see cref="PngColorType"/> for the given <typeparamref name="TPixel"/>
/// This is not exhaustive but covers many common pixel formats.
/// </summary>
private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
return typeof(TPixel) switch
{
Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Bgr24) => PngColorType.Rgb,
Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(L8) => PngColorType.Grayscale,
Type t when t == typeof(L16) => PngColorType.Grayscale,
Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Rgb24) => PngColorType.Rgb,
Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Rgb48) => PngColorType.Rgb,
Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha,
Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha
};
}
/// <summary>
/// Returns a suggested <see cref="PngBitDepth"/> for the given <typeparamref name="TPixel"/>
/// This is not exhaustive but covers many common pixel formats.
/// </summary>
private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
return typeof(TPixel) switch
{
Type t when t == typeof(A8) => PngBitDepth.Bit8,
Type t when t == typeof(Argb32) => PngBitDepth.Bit8,
Type t when t == typeof(Bgr24) => PngBitDepth.Bit8,
Type t when t == typeof(Bgra32) => PngBitDepth.Bit8,
Type t when t == typeof(L8) => PngBitDepth.Bit8,
Type t when t == typeof(L16) => PngBitDepth.Bit16,
Type t when t == typeof(La16) => PngBitDepth.Bit8,
Type t when t == typeof(La32) => PngBitDepth.Bit16,
Type t when t == typeof(Rgb24) => PngBitDepth.Bit8,
Type t when t == typeof(Rgba32) => PngBitDepth.Bit8,
Type t when t == typeof(Rgb48) => PngBitDepth.Bit16,
Type t when t == typeof(Rgba64) => PngBitDepth.Bit16,
Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8
};
}
}

20
src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary>
/// Configuration options for use during tga encoding.
/// </summary>
internal interface ITgaEncoderOptions
{
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
TgaBitsPerPixel? BitsPerPixel { get; }
/// <summary>
/// Gets a value indicating whether run length compression should be used.
/// </summary>
TgaCompression Compression { get; }
}

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

@ -2,38 +2,35 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary>
/// Image encoder for writing an image to a stream as a targa truevision image.
/// </summary>
public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions
public sealed class TgaEncoder : ImageEncoder
{
/// <summary>
/// Gets or sets the number of bits per pixel.
/// Gets the number of bits per pixel.
/// </summary>
public TgaBitsPerPixel? BitsPerPixel { get; set; }
public TgaBitsPerPixel? BitsPerPixel { get; init; }
/// <summary>
/// Gets or sets a value indicating whether no compression or run length compression should be used.
/// Gets a value indicating whether no compression or run length compression should be used.
/// </summary>
public TgaCompression Compression { get; set; } = TgaCompression.RunLength;
public TgaCompression Compression { get; init; } = TgaCompression.RunLength;
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator());
TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator());
TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

12
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -45,13 +45,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <summary>
/// Initializes a new instance of the <see cref="TgaEncoderCore"/> class.
/// </summary>
/// <param name="options">The encoder options.</param>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator)
public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.compression = options.Compression;
this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = encoder.Compression;
}
/// <summary>
@ -105,7 +105,9 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
cMapLength: 0,
cMapDepth: 0,
xOffset: 0,
yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left.
// When run length encoding is used, the origin should be top left instead of the default bottom left.
yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0,
width: (short)image.Width,
height: (short)image.Height,
pixelDepth: (byte)this.bitsPerPixel.Value,

46
src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs

@ -1,46 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Encapsulates the options for the <see cref="TiffEncoder"/>.
/// </summary>
internal interface ITiffEncoderOptions
{
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
TiffBitsPerPixel? BitsPerPixel { get; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
TiffCompression? Compression { get; }
/// <summary>
/// Gets the compression level 1-9 for the deflate compression mode.
/// <remarks>Defaults to <see cref="DeflateCompressionLevel.DefaultCompression"/>.</remarks>
/// </summary>
DeflateCompressionLevel? CompressionLevel { get; }
/// <summary>
/// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor.
/// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used.
/// </summary>
TiffPhotometricInterpretation? PhotometricInterpretation { get; }
/// <summary>
/// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression.
/// </summary>
TiffPredictor? HorizontalPredictor { get; }
/// <summary>
/// Gets the quantizer for creating a color palette image.
/// </summary>
IQuantizer Quantizer { get; }
}

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

@ -4,47 +4,52 @@
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Encoder for writing the data image to a stream in TIFF format.
/// </summary>
public class TiffEncoder : IImageEncoder, ITiffEncoderOptions
public class TiffEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
public TiffBitsPerPixel? BitsPerPixel { get; set; }
/// <inheritdoc/>
public TiffCompression? Compression { get; set; }
/// <inheritdoc/>
public DeflateCompressionLevel? CompressionLevel { get; set; }
/// <inheritdoc/>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; }
/// <inheritdoc/>
public TiffPredictor? HorizontalPredictor { get; set; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; init; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
public TiffCompression? Compression { get; init; }
/// <summary>
/// Gets the compression level 1-9 for the deflate compression mode.
/// <remarks>Defaults to <see cref="DeflateCompressionLevel.DefaultCompression" />.</remarks>
/// </summary>
public DeflateCompressionLevel? CompressionLevel { get; init; }
/// <summary>
/// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor.
/// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used.
/// </summary>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; init; }
/// <summary>
/// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression.
/// </summary>
public TiffPredictor? HorizontalPredictor { get; init; }
/// <inheritdoc/>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encode = new TiffEncoderCore(this, image.GetMemoryAllocator());
TiffEncoderCore encode = new(this, image.GetMemoryAllocator());
encode.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new TiffEncoderCore(this, image.GetMemoryAllocator());
TiffEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

29
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -40,10 +39,15 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
private Configuration configuration;
/// <summary>
/// The quantizer for creating color palette image.
/// The quantizer for creating color palette images.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// The pixel sampling strategy for quantization.
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// Sets the deflate compression level.
/// </summary>
@ -69,6 +73,11 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary>
private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
private readonly List<(long, uint)> frameMarkers = new();
/// <summary>
@ -76,15 +85,17 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary>
/// <param name="options">The options for the encoder.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator)
public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = options.Quantizer;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
this.CompressionType = options.Compression;
this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
this.skipMetadata = options.SkipMetadata;
}
/// <summary>
@ -215,6 +226,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
this.PhotometricInterpretation,
frame,
this.quantizer,
this.pixelSamplingStrategy,
this.memoryAllocator,
this.configuration,
entriesCollector,
@ -226,7 +238,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
if (image != null)
{
entriesCollector.ProcessMetadata(image);
entriesCollector.ProcessMetadata(image, this.skipMetadata);
}
entriesCollector.ProcessFrameInfo(frame, imageMetadata);
@ -331,7 +343,12 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return nextIfdMarker;
}
private void SanitizeAndSetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, int inputBitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor)
private void SanitizeAndSetEncoderOptions(
TiffBitsPerPixel? bitsPerPixel,
int inputBitsPerPixel,
TiffPhotometricInterpretation? photometricInterpretation,
TiffCompression compression,
TiffPredictor predictor)
{
// BitsPerPixel should be the primary source of truth for the encoder options.
if (bitsPerPixel.HasValue)

153
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -15,8 +15,8 @@ internal class TiffEncoderEntriesCollector
public List<IExifValue> Entries { get; } = new List<IExifValue>();
public void ProcessMetadata(Image image)
=> new MetadataProcessor(this).Process(image);
public void ProcessMetadata(Image image, bool skipMetadata)
=> new MetadataProcessor(this).Process(image, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata);
@ -41,7 +41,7 @@ internal class TiffEncoderEntriesCollector
private abstract class BaseProcessor
{
public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector;
protected BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector;
protected TiffEncoderEntriesCollector Collector { get; }
}
@ -53,14 +53,18 @@ internal class TiffEncoderEntriesCollector
{
}
public void Process(Image image)
public void Process(Image image, bool skipMetadata)
{
ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile();
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile);
this.ProcessMetadata(rootFrameExifProfile);
this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile);
if (!skipMetadata)
{
this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile());
}
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
{
@ -72,39 +76,35 @@ internal class TiffEncoderEntriesCollector
}
private static bool IsPureMetadata(ExifTag tag)
{
switch ((ExifTagValue)(ushort)tag)
=> (ExifTagValue)(ushort)tag switch
{
case ExifTagValue.DocumentName:
case ExifTagValue.ImageDescription:
case ExifTagValue.Make:
case ExifTagValue.Model:
case ExifTagValue.Software:
case ExifTagValue.DateTime:
case ExifTagValue.Artist:
case ExifTagValue.HostComputer:
case ExifTagValue.TargetPrinter:
case ExifTagValue.XMP:
case ExifTagValue.Rating:
case ExifTagValue.RatingPercent:
case ExifTagValue.ImageID:
case ExifTagValue.Copyright:
case ExifTagValue.MDLabName:
case ExifTagValue.MDSampleInfo:
case ExifTagValue.MDPrepDate:
case ExifTagValue.MDPrepTime:
case ExifTagValue.MDFileUnits:
case ExifTagValue.SEMInfo:
case ExifTagValue.XPTitle:
case ExifTagValue.XPComment:
case ExifTagValue.XPAuthor:
case ExifTagValue.XPKeywords:
case ExifTagValue.XPSubject:
return true;
default:
return false;
}
}
ExifTagValue.DocumentName or
ExifTagValue.ImageDescription or
ExifTagValue.Make or
ExifTagValue.Model or
ExifTagValue.Software or
ExifTagValue.DateTime or
ExifTagValue.Artist or
ExifTagValue.HostComputer or
ExifTagValue.TargetPrinter or
ExifTagValue.XMP or
ExifTagValue.Rating or
ExifTagValue.RatingPercent or
ExifTagValue.ImageID or
ExifTagValue.Copyright or
ExifTagValue.MDLabName or
ExifTagValue.MDSampleInfo or
ExifTagValue.MDPrepDate or
ExifTagValue.MDPrepTime or
ExifTagValue.MDFileUnits or
ExifTagValue.SEMInfo or
ExifTagValue.XPTitle or
ExifTagValue.XPComment or
ExifTagValue.XPAuthor or
ExifTagValue.XPKeywords or
ExifTagValue.XPSubject => true,
_ => false,
};
private void ProcessMetadata(ExifProfile exifProfile)
{
@ -149,9 +149,9 @@ internal class TiffEncoderEntriesCollector
}
}
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
{
if (exifProfile != null && exifProfile.Parts != ExifParts.None)
if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
{
foreach (IExifValue entry in exifProfile.Values)
{
@ -167,13 +167,13 @@ internal class TiffEncoderEntriesCollector
}
else
{
exifProfile.RemoveValue(ExifTag.SubIFDOffset);
exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
}
if (imageMetadata.IptcProfile != null)
if (!skipMetadata && imageMetadata.IptcProfile != null)
{
imageMetadata.IptcProfile.UpdateData();
var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte)
ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
{
Value = imageMetadata.IptcProfile.Data
};
@ -182,12 +182,12 @@ internal class TiffEncoderEntriesCollector
}
else
{
exifProfile.RemoveValue(ExifTag.IPTC);
exifProfile?.RemoveValue(ExifTag.IPTC);
}
if (imageMetadata.IccProfile != null)
{
var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined)
ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
{
Value = imageMetadata.IccProfile.ToByteArray()
};
@ -196,12 +196,12 @@ internal class TiffEncoderEntriesCollector
}
else
{
exifProfile.RemoveValue(ExifTag.IccProfile);
exifProfile?.RemoveValue(ExifTag.IccProfile);
}
if (xmpProfile != null)
if (!skipMetadata && xmpProfile != null)
{
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)
{
Value = xmpProfile.Data
};
@ -210,7 +210,7 @@ internal class TiffEncoderEntriesCollector
}
else
{
exifProfile.RemoveValue(ExifTag.XMP);
exifProfile?.RemoveValue(ExifTag.XMP);
}
}
}
@ -273,29 +273,29 @@ internal class TiffEncoderEntriesCollector
public void Process(TiffEncoderCore encoder)
{
var planarConfig = new ExifShort(ExifTagValue.PlanarConfiguration)
ExifShort planarConfig = new(ExifTagValue.PlanarConfiguration)
{
Value = (ushort)TiffPlanarConfiguration.Chunky
};
var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel)
ExifLong samplesPerPixel = new(ExifTagValue.SamplesPerPixel)
{
Value = GetSamplesPerPixel(encoder)
};
ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder);
var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample)
ExifShortArray bitPerSample = new(ExifTagValue.BitsPerSample)
{
Value = bitsPerSampleValue
};
ushort compressionType = GetCompressionType(encoder);
var compression = new ExifShort(ExifTagValue.Compression)
ExifShort compression = new(ExifTagValue.Compression)
{
Value = compressionType
};
var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation)
ExifShort photometricInterpretation = new(ExifTagValue.PhotometricInterpretation)
{
Value = (ushort)encoder.PhotometricInterpretation
};
@ -306,32 +306,25 @@ internal class TiffEncoderEntriesCollector
this.Collector.AddOrReplace(compression);
this.Collector.AddOrReplace(photometricInterpretation);
if (encoder.HorizontalPredictor == TiffPredictor.Horizontal)
if (encoder.HorizontalPredictor == TiffPredictor.Horizontal &&
(encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or
TiffPhotometricInterpretation.PaletteColor or
TiffPhotometricInterpretation.BlackIsZero))
{
if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero)
{
var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal };
ExifShort predictor = new(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal };
this.Collector.AddOrReplace(predictor);
}
this.Collector.AddOrReplace(predictor);
}
}
private static uint GetSamplesPerPixel(TiffEncoderCore encoder)
{
switch (encoder.PhotometricInterpretation)
=> encoder.PhotometricInterpretation switch
{
case TiffPhotometricInterpretation.PaletteColor:
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
return 1;
case TiffPhotometricInterpretation.Rgb:
default:
return 3;
}
}
TiffPhotometricInterpretation.PaletteColor or
TiffPhotometricInterpretation.BlackIsZero or
TiffPhotometricInterpretation.WhiteIsZero => 1,
_ => 3,
};
private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder)
{
@ -342,10 +335,8 @@ internal class TiffEncoderEntriesCollector
{
return TiffConstants.BitsPerSample4Bit.ToArray();
}
else
{
return TiffConstants.BitsPerSample8Bit.ToArray();
}
return TiffConstants.BitsPerSample8Bit.ToArray();
case TiffPhotometricInterpretation.Rgb:
return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
@ -382,9 +373,9 @@ internal class TiffEncoderEntriesCollector
// PackBits is allowed for all modes.
return (ushort)TiffCompression.PackBits;
case TiffCompression.Lzw:
if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero)
if (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or
TiffPhotometricInterpretation.PaletteColor or
TiffPhotometricInterpretation.BlackIsZero)
{
return (ushort)TiffCompression.Lzw;
}

3
src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs

@ -14,6 +14,7 @@ internal static class TiffColorWriterFactory
TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame<TPixel> image,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector,
@ -23,7 +24,7 @@ internal static class TiffColorWriterFactory
switch (photometricInterpretation)
{
case TiffPhotometricInterpretation.PaletteColor:
return new TiffPaletteWriter<TPixel>(image, quantizer, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
return new TiffPaletteWriter<TPixel>(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
if (bitsPerPixel == 1)

34
src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs

@ -17,19 +17,21 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
private readonly int maxColors;
private readonly int colorPaletteSize;
private readonly int colorPaletteBytes;
private readonly IndexedImageFrame<TPixel> quantizedImage;
private readonly IndexedImageFrame<TPixel> quantizedFrame;
private IMemoryOwner<byte> indexedPixelsBuffer;
public TiffPaletteWriter(
ImageFrame<TPixel> image,
ImageFrame<TPixel> frame,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
: base(image, memoryAllocator, configuration, entriesCollector)
: base(frame, memoryAllocator, configuration, entriesCollector)
{
DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
DebugGuard.NotNull(configuration, nameof(configuration));
DebugGuard.NotNull(entriesCollector, nameof(entriesCollector));
DebugGuard.MustBeBetweenOrEqualTo(bitsPerPixel, 4, 8, nameof(bitsPerPixel));
@ -38,11 +40,15 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
this.maxColors = this.BitsPerPixel == 4 ? 16 : 256;
this.colorPaletteSize = this.maxColors * 3;
this.colorPaletteBytes = this.colorPaletteSize * 2;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration, new QuantizerOptions()
{
MaxColors = this.maxColors
});
this.quantizedImage = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(
this.Configuration,
new QuantizerOptions()
{
MaxColors = this.maxColors
});
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.AddColorMapTag();
}
@ -66,7 +72,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
int lastRow = y + height;
for (int row = y; row < lastRow; row++)
{
ReadOnlySpan<byte> indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row);
ReadOnlySpan<byte> indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row);
int idxPixels = 0;
for (int x = 0; x < halfWidth; x++)
{
@ -93,7 +99,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
int indexedPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
ReadOnlySpan<byte> indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row);
ReadOnlySpan<byte> indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row);
indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width));
indexedPixelsRowIdx++;
}
@ -105,7 +111,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
this.quantizedImage?.Dispose();
this.quantizedFrame?.Dispose();
this.indexedPixelsBuffer?.Dispose();
}
@ -114,7 +120,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
using IMemoryOwner<byte> colorPaletteBuffer = this.MemoryAllocator.Allocate<byte>(this.colorPaletteBytes);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColors = this.quantizedImage.Palette.Span;
ReadOnlySpan<TPixel> quantizedColors = this.quantizedFrame.Palette.Span;
int quantizedColorBytes = quantizedColors.Length * 3 * 2;
// In the ColorMap, black is represented by 0, 0, 0 and white is represented by 65535, 65535, 65535.
@ -126,7 +132,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
// In a TIFF ColorMap, all the Red values come first, followed by the Green values,
// then the Blue values. Convert the quantized palette to this format.
var palette = new ushort[this.colorPaletteSize];
ushort[] palette = new ushort[this.colorPaletteSize];
int paletteIdx = 0;
for (int i = 0; i < quantizedColors.Length; i++)
{
@ -147,7 +153,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
palette[paletteIdx++] = quantizedColorRgb48[i].B;
}
var colorMap = new ExifShortArray(ExifTagValue.ColorMap)
ExifShortArray colorMap = new(ExifTagValue.ColorMap)
{
Value = palette
};

12
src/ImageSharp/Formats/Webp/AlphaEncoder.cs

@ -24,10 +24,11 @@ internal class AlphaEncoder : IDisposable
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <param name="skipMetadata">Whether to skip metadata encoding.</param>
/// <param name="compress">Indicates, if the data should be compressed with the lossless webp compression.</param>
/// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns>
public IMemoryOwner<byte> EncodeAlpha<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size)
public IMemoryOwner<byte> EncodeAlpha<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, bool compress, out int size)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -36,14 +37,15 @@ internal class AlphaEncoder : IDisposable
if (compress)
{
WebpEncodingMethod effort = WebpEncodingMethod.Default;
int quality = 8 * (int)effort;
using var lossLessEncoder = new Vp8LEncoder(
const WebpEncodingMethod effort = WebpEncodingMethod.Default;
const int quality = 8 * (int)effort;
using Vp8LEncoder lossLessEncoder = new(
memoryAllocator,
configuration,
width,
height,
quality,
skipMetadata,
effort,
WebpTransparentColorMode.Preserve,
false,
@ -75,7 +77,7 @@ internal class AlphaEncoder : IDisposable
{
int width = image.Width;
int height = image.Height;
var alphaAsImage = new Image<Rgba32>(width, height);
Image<Rgba32> alphaAsImage = new(width, height);
for (int y = 0; y < height; y++)
{

79
src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs

@ -1,79 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Configuration options for use during webp encoding.
/// </summary>
internal interface IWebpEncoderOptions
{
/// <summary>
/// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.
/// </summary>
WebpFileFormatType? FileFormat { get; }
/// <summary>
/// Gets the compression quality. Between 0 and 100.
/// For lossy, 0 gives the smallest size and 100 the largest. For lossless,
/// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger
/// files compared to the slowest, but best, 100.
/// Defaults to 75.
/// </summary>
int Quality { get; }
/// <summary>
/// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better).
/// Defaults to 4.
/// </summary>
WebpEncodingMethod Method { get; }
/// <summary>
/// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format.
/// Defaults to true.
/// </summary>
bool UseAlphaCompression { get; }
/// <summary>
/// Gets the number of entropy-analysis passes (in [1..10]).
/// Defaults to 1.
/// </summary>
int EntropyPasses { get; }
/// <summary>
/// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms
/// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits.
/// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect).
/// Defaults to 50.
/// </summary>
int SpatialNoiseShaping { get; }
/// <summary>
/// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).
/// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture.
/// The higher the value the smoother the picture will appear.
/// Typical values are usually in the range of 20 to 50.
/// Defaults to 60.
/// </summary>
int FilterStrength { get; }
/// <summary>
/// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// The default value is Clear.
/// </summary>
WebpTransparentColorMode TransparentColorMode { get; }
/// <summary>
/// Gets a value indicating whether near lossless mode should be used.
/// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.
/// </summary>
bool NearLossless { get; }
/// <summary>
/// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default).
/// The typical value is around 60. Note that lossy with -q 100 can at times yield better results.
/// </summary>
int NearLosslessQuality { get; }
}

15
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -8,6 +8,8 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@ -67,6 +69,11 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// Indicating whether near lossless mode should be used.
/// </summary>
@ -91,6 +98,7 @@ internal class Vp8LEncoder : IDisposable
/// <param name="width">The width of the input image.</param>
/// <param name="height">The height of the input image.</param>
/// <param name="quality">The encoding quality.</param>
/// <param name="skipMetadata">Whether to skip metadata encoding.</param>
/// <param name="method">Quality/speed trade-off (0=fast, 6=slower-better).</param>
/// <param name="transparentColorMode">Flag indicating whether to preserve the exact RGB values under transparent area.
/// Otherwise, discard this invisible RGB information for better compression.</param>
@ -102,6 +110,7 @@ internal class Vp8LEncoder : IDisposable
int width,
int height,
int quality,
bool skipMetadata,
WebpEncodingMethod method,
WebpTransparentColorMode transparentColorMode,
bool nearLossless,
@ -113,6 +122,7 @@ internal class Vp8LEncoder : IDisposable
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.quality = Numerics.Clamp(quality, 0, 100);
this.skipMetadata = skipMetadata;
this.method = method;
this.transparentColorMode = transparentColorMode;
this.nearLossless = nearLossless;
@ -239,6 +249,9 @@ internal class Vp8LEncoder : IDisposable
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
// Convert image pixels to bgra array.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
@ -252,7 +265,7 @@ internal class Vp8LEncoder : IDisposable
this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
}
/// <summary>

20
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -6,6 +6,8 @@ using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
@ -55,6 +57,11 @@ internal class Vp8Encoder : IDisposable
/// </summary>
private Vp8BitWriter bitWriter;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
private readonly Vp8RdLevel rdOptLevel;
private int maxI4HeaderBits;
@ -94,6 +101,7 @@ internal class Vp8Encoder : IDisposable
/// <param name="width">The width of the input image.</param>
/// <param name="height">The height of the input image.</param>
/// <param name="quality">The encoding quality.</param>
/// <param name="skipMetadata">Whether to skip metadata encoding.</param>
/// <param name="method">Quality/speed trade-off (0=fast, 6=slower-better).</param>
/// <param name="entropyPasses">Number of entropy-analysis passes (in [1..10]).</param>
/// <param name="filterStrength">The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).</param>
@ -105,6 +113,7 @@ internal class Vp8Encoder : IDisposable
int width,
int height,
int quality,
bool skipMetadata,
WebpEncodingMethod method,
int entropyPasses,
int filterStrength,
@ -116,6 +125,7 @@ internal class Vp8Encoder : IDisposable
this.Width = width;
this.Height = height;
this.quality = Numerics.Clamp(quality, 0, 100);
this.skipMetadata = skipMetadata;
this.method = method;
this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10);
this.filterStrength = Numerics.Clamp(filterStrength, 0, 100);
@ -342,7 +352,7 @@ internal class Vp8Encoder : IDisposable
if (hasAlpha)
{
// TODO: This can potentially run in an separate task.
IMemoryOwner<byte> encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize);
IMemoryOwner<byte> encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.skipMetadata, this.alphaCompression, out alphaDataSize);
alphaData = encodedAlphaData.GetSpan();
if (alphaDataSize < pixelCount)
{
@ -384,10 +394,14 @@ internal class Vp8Encoder : IDisposable
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
this.bitWriter.WriteEncodedImageToStream(
stream,
metadata.ExifProfile,
metadata.XmpProfile,
exifProfile,
xmpProfile,
metadata.IccProfile,
(uint)width,
(uint)height,

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

@ -2,58 +2,94 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Image encoder for writing an image to a stream in the Webp format.
/// </summary>
public sealed class WebpEncoder : IImageEncoder, IWebpEncoderOptions
public sealed class WebpEncoder : ImageEncoder
{
/// <inheritdoc/>
public WebpFileFormatType? FileFormat { get; set; }
/// <summary>
/// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.
/// </summary>
public WebpFileFormatType? FileFormat { get; init; }
/// <inheritdoc/>
public int Quality { get; set; } = 75;
/// <summary>
/// Gets the compression quality. Between 0 and 100.
/// For lossy, 0 gives the smallest size and 100 the largest. For lossless,
/// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger
/// files compared to the slowest, but best, 100.
/// Defaults to 75.
/// </summary>
public int Quality { get; init; } = 75;
/// <inheritdoc/>
public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default;
/// <summary>
/// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better).
/// Defaults to 4.
/// </summary>
public WebpEncodingMethod Method { get; init; } = WebpEncodingMethod.Default;
/// <inheritdoc/>
public bool UseAlphaCompression { get; set; } = true;
/// <summary>
/// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format.
/// Defaults to true.
/// </summary>
public bool UseAlphaCompression { get; init; } = true;
/// <inheritdoc/>
public int EntropyPasses { get; set; } = 1;
/// <summary>
/// Gets the number of entropy-analysis passes (in [1..10]).
/// Defaults to 1.
/// </summary>
public int EntropyPasses { get; init; } = 1;
/// <inheritdoc/>
public int SpatialNoiseShaping { get; set; } = 50;
/// <summary>
/// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms
/// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits.
/// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect).
/// Defaults to 50.
/// </summary>
public int SpatialNoiseShaping { get; init; } = 50;
/// <inheritdoc/>
public int FilterStrength { get; set; } = 60;
/// <summary>
/// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).
/// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture.
/// The higher the value the smoother the picture will appear.
/// Typical values are usually in the range of 20 to 50.
/// Defaults to 60.
/// </summary>
public int FilterStrength { get; init; } = 60;
/// <inheritdoc/>
public WebpTransparentColorMode TransparentColorMode { get; set; } = WebpTransparentColorMode.Clear;
/// <summary>
/// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// The default value is Clear.
/// </summary>
public WebpTransparentColorMode TransparentColorMode { get; init; } = WebpTransparentColorMode.Clear;
/// <inheritdoc/>
public bool NearLossless { get; set; }
/// <summary>
/// Gets a value indicating whether near lossless mode should be used.
/// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.
/// </summary>
public bool NearLossless { get; init; }
/// <inheritdoc/>
public int NearLosslessQuality { get; set; } = 100;
/// <summary>
/// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default).
/// The typical value is around 60. Note that lossy with -q 100 can at times yield better results.
/// </summary>
public int NearLosslessQuality { get; init; } = 100;
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator());
WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator());
WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}

36
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -56,6 +56,11 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// Indicating whether near lossless mode should be used.
/// </summary>
@ -80,21 +85,22 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
/// <summary>
/// Initializes a new instance of the <see cref="WebpEncoderCore"/> class.
/// </summary>
/// <param name="options">The encoder options.</param>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public WebpEncoderCore(IWebpEncoderOptions options, MemoryAllocator memoryAllocator)
public WebpEncoderCore(WebpEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.alphaCompression = options.UseAlphaCompression;
this.fileFormat = options.FileFormat;
this.quality = options.Quality;
this.method = options.Method;
this.entropyPasses = options.EntropyPasses;
this.spatialNoiseShaping = options.SpatialNoiseShaping;
this.filterStrength = options.FilterStrength;
this.transparentColorMode = options.TransparentColorMode;
this.nearLossless = options.NearLossless;
this.nearLosslessQuality = options.NearLosslessQuality;
this.alphaCompression = encoder.UseAlphaCompression;
this.fileFormat = encoder.FileFormat;
this.quality = encoder.Quality;
this.method = encoder.Method;
this.entropyPasses = encoder.EntropyPasses;
this.spatialNoiseShaping = encoder.SpatialNoiseShaping;
this.filterStrength = encoder.FilterStrength;
this.transparentColorMode = encoder.TransparentColorMode;
this.skipMetadata = encoder.SkipMetadata;
this.nearLossless = encoder.NearLossless;
this.nearLosslessQuality = encoder.NearLosslessQuality;
}
/// <summary>
@ -124,12 +130,13 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless)
{
using var enc = new Vp8LEncoder(
using Vp8LEncoder enc = new(
this.memoryAllocator,
this.configuration,
image.Width,
image.Height,
this.quality,
this.skipMetadata,
this.method,
this.transparentColorMode,
this.nearLossless,
@ -138,12 +145,13 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
}
else
{
using var enc = new Vp8Encoder(
using Vp8Encoder enc = new(
this.memoryAllocator,
this.configuration,
image.Width,
image.Height,
this.quality,
this.skipMetadata,
this.method,
this.entropyPasses,
this.filterStrength,

10
src/ImageSharp/ImageExtensions.Internal.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp;
public static partial class ImageExtensions
{
/// <summary>
/// Locks the image providing access to the pixels.
/// Provides access to the image pixels.
/// <remarks>
/// It is imperative that the accessor is correctly disposed off after use.
/// It is imperative that the accessor is correctly disposed of after use.
/// </remarks>
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
@ -24,7 +24,5 @@ public static partial class ImageExtensions
/// </returns>
internal static Buffer2D<TPixel> GetRootFramePixelBuffer<TPixel>(this Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
return image.Frames.RootFrame.PixelBuffer;
}
=> image.Frames.RootFrame.PixelBuffer;
}

6
src/ImageSharp/ImageFrameCollection.cs

@ -180,7 +180,7 @@ public abstract class ImageFrameCollection : IDisposable, IEnumerable<ImageFrame
}
/// <inheritdoc />
public IEnumerator<ImageFrame> GetEnumerator()
IEnumerator<ImageFrame> IEnumerable<ImageFrame>.GetEnumerator()
{
this.EnsureNotDisposed();
@ -188,7 +188,7 @@ public abstract class ImageFrameCollection : IDisposable, IEnumerable<ImageFrame
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<ImageFrame>)this).GetEnumerator();
/// <summary>
/// Throws <see cref="ObjectDisposedException"/> if the image frame is disposed.
@ -208,7 +208,7 @@ public abstract class ImageFrameCollection : IDisposable, IEnumerable<ImageFrame
protected abstract void Dispose(bool disposing);
/// <summary>
/// Implements <see cref="GetEnumerator"/>.
/// Implements <see cref="IEnumerable{ImageFrame}.GetEnumerator"/>.
/// </summary>
/// <returns>The enumerator.</returns>
protected abstract IEnumerator<ImageFrame> NonGenericGetEnumerator();

17
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -168,7 +168,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{
this.EnsureNotDisposed();
var frame = ImageFrame.LoadPixelData(
ImageFrame<TPixel> frame = ImageFrame.LoadPixelData(
this.parent.GetConfiguration(),
source,
this.RootFrame.Width,
@ -298,7 +298,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{
this.EnsureNotDisposed();
var frame = new ImageFrame<TPixel>(
ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(),
this.RootFrame.Width,
this.RootFrame.Height);
@ -364,7 +364,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
/// </returns>
public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor)
{
var frame = new ImageFrame<TPixel>(
ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(),
this.RootFrame.Width,
this.RootFrame.Height,
@ -374,10 +374,15 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
}
/// <inheritdoc/>
IEnumerator<ImageFrame<TPixel>> IEnumerable<ImageFrame<TPixel>>.GetEnumerator() => this.frames.GetEnumerator();
public IEnumerator<ImageFrame<TPixel>> GetEnumerator()
{
this.EnsureNotDisposed();
return this.frames.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.frames).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private void ValidateFrame(ImageFrame<TPixel> frame)
{
@ -408,7 +413,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source)
{
var result = new ImageFrame<TPixel>(
ImageFrame<TPixel> result = new(
this.parent.GetConfiguration(),
source.Size(),
source.Metadata.DeepClone());

43
src/ImageSharp/Image{TPixel}.cs

@ -80,9 +80,7 @@ public sealed class Image<TPixel> : Image
/// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
}
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -115,9 +113,7 @@ public sealed class Image<TPixel> : Image
int height,
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.frames = new ImageFrameCollection<TPixel>(this, width, height, memoryGroup);
}
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, memoryGroup);
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -135,9 +131,7 @@ public sealed class Image<TPixel> : Image
TPixel backgroundColor,
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
}
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}" /> class
@ -148,9 +142,7 @@ public sealed class Image<TPixel> : Image
/// <param name="frames">The frames that will be owned by this image instance.</param>
internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<ImageFrame<TPixel>> frames)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, ValidateFramesAndGetSize(frames))
{
this.frames = new ImageFrameCollection<TPixel>(this, frames);
}
=> this.frames = new ImageFrameCollection<TPixel>(this, frames);
/// <inheritdoc />
protected override ImageFrameCollection NonGenericFrameCollection => this.Frames;
@ -181,7 +173,7 @@ public sealed class Image<TPixel> : Image
/// <exception cref="ArgumentOutOfRangeException">Thrown when the provided (x,y) coordinates are outside the image boundary.</exception>
public TPixel this[int x, int y]
{
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
this.EnsureNotDisposed();
@ -190,7 +182,7 @@ public sealed class Image<TPixel> : Image
return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y);
}
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
this.EnsureNotDisposed();
@ -212,7 +204,7 @@ public sealed class Image<TPixel> : Image
try
{
var accessor = new PixelAccessor<TPixel>(buffer);
PixelAccessor<TPixel> accessor = new(buffer);
processPixels(accessor);
}
finally
@ -243,8 +235,8 @@ public sealed class Image<TPixel> : Image
try
{
var accessor1 = new PixelAccessor<TPixel>(buffer1);
var accessor2 = new PixelAccessor<TPixel2>(buffer2);
PixelAccessor<TPixel> accessor1 = new(buffer1);
PixelAccessor<TPixel2> accessor2 = new(buffer2);
processPixels(accessor1, accessor2);
}
finally
@ -283,9 +275,9 @@ public sealed class Image<TPixel> : Image
try
{
var accessor1 = new PixelAccessor<TPixel>(buffer1);
var accessor2 = new PixelAccessor<TPixel2>(buffer2);
var accessor3 = new PixelAccessor<TPixel3>(buffer3);
PixelAccessor<TPixel> accessor1 = new(buffer1);
PixelAccessor<TPixel2> accessor2 = new(buffer2);
PixelAccessor<TPixel3> accessor3 = new(buffer3);
processPixels(accessor1, accessor2, accessor3);
}
finally
@ -348,7 +340,7 @@ public sealed class Image<TPixel> : Image
{
this.EnsureNotDisposed();
var clonedFrames = new ImageFrame<TPixel>[this.frames.Count];
ImageFrame<TPixel>[] clonedFrames = new ImageFrame<TPixel>[this.frames.Count];
for (int i = 0; i < clonedFrames.Length; i++)
{
clonedFrames[i] = this.frames[i].Clone(configuration);
@ -367,7 +359,7 @@ public sealed class Image<TPixel> : Image
{
this.EnsureNotDisposed();
var clonedFrames = new ImageFrame<TPixel2>[this.frames.Count];
ImageFrame<TPixel2>[] clonedFrames = new ImageFrame<TPixel2>[this.frames.Count];
for (int i = 0; i < clonedFrames.Length; i++)
{
clonedFrames[i] = this.frames[i].CloneAs<TPixel2>(configuration);
@ -444,7 +436,7 @@ public sealed class Image<TPixel> : Image
return rootSize;
}
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void VerifyCoords(int x, int y)
{
if ((uint)x >= (uint)this.Width)
@ -458,9 +450,6 @@ public sealed class Image<TPixel> : Image
}
}
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowArgumentOutOfRangeException(string paramName)
{
throw new ArgumentOutOfRangeException(paramName);
}
=> throw new ArgumentOutOfRangeException(paramName);
}

56
src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs

@ -79,14 +79,15 @@ public class DefaultPixelSamplingStrategy : IPixelSamplingStrategy
r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image.
var ratio = new Rational(r);
Rational ratio = new(r);
int denom = (int)ratio.Denominator;
int num = (int)ratio.Numerator;
DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero.");
for (int pos = 0; pos < totalNumberOfRows; pos++)
{
int subPos = pos % denom;
int subPos = (int)((uint)pos % (uint)denom);
if (subPos < num)
{
yield return GetRow(pos);
@ -101,4 +102,55 @@ public class DefaultPixelSamplingStrategy : IPixelSamplingStrategy
}
}
}
/// <inheritdoc />
public IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
long maximumPixels = Math.Min(this.MaximumPixels, (long)frame.Width * frame.Height);
long maxNumberOfRows = maximumPixels / frame.Width;
long totalNumberOfRows = frame.Height;
if (totalNumberOfRows <= maxNumberOfRows)
{
yield return frame.PixelBuffer.GetRegion();
}
else
{
double r = maxNumberOfRows / (double)totalNumberOfRows;
// Use a rough approximation to make sure we don't leave out large contiguous regions:
if (maxNumberOfRows > 200)
{
r = Math.Round(r, 2);
}
else
{
r = Math.Round(r, 1);
}
r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image.
Rational ratio = new(r);
int denom = (int)ratio.Denominator;
int num = (int)ratio.Numerator;
DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero.");
for (int pos = 0; pos < totalNumberOfRows; pos++)
{
int subPos = (int)((uint)pos % (uint)denom);
if (subPos < num)
{
yield return GetRow(pos);
}
}
Buffer2DRegion<TPixel> GetRow(int pos)
{
int y = pos % frame.Height;
return frame.PixelBuffer.GetRegion(0, y, frame.Width, 1);
}
}
}
}

7
src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs

@ -20,4 +20,11 @@ public class ExtensivePixelSamplingStrategy : IPixelSamplingStrategy
yield return frame.PixelBuffer.GetRegion();
}
}
/// <inheritdoc/>
public IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
yield return frame.PixelBuffer.GetRegion();
}
}

13
src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs

@ -7,16 +7,25 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// <summary>
/// Provides an abstraction to enumerate pixel regions within a multi-framed <see cref="Image{TPixel}"/>.
/// Provides an abstraction to enumerate pixel regions for sampling within <see cref="Image{TPixel}"/>.
/// </summary>
public interface IPixelSamplingStrategy
{
/// <summary>
/// Enumerates pixel regions within the image as <see cref="Buffer2DRegion{T}"/>.
/// Enumerates pixel regions for all frames within the image as <see cref="Buffer2DRegion{T}"/>.
/// </summary>
/// <param name="image">The image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>An enumeration of pixel regions.</returns>
IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>;
/// <summary>
/// Enumerates pixel regions within a single image frame as <see cref="Buffer2DRegion{T}"/>.
/// </summary>
/// <param name="frame">The image frame.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>An enumeration of pixel regions.</returns>
IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>;
}

38
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -50,7 +50,7 @@ public static class QuantizerUtilities
Guard.NotNull(quantizer, nameof(quantizer));
Guard.NotNull(source, nameof(source));
var interest = Rectangle.Intersect(source.Bounds(), bounds);
Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds);
Buffer2DRegion<TPixel> region = source.PixelBuffer.GetRegion(interest);
// Collect the palette. Required before the second pass runs.
@ -77,9 +77,9 @@ public static class QuantizerUtilities
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(source, nameof(source));
var interest = Rectangle.Intersect(source.Bounds(), bounds);
Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds);
var destination = new IndexedImageFrame<TPixel>(
IndexedImageFrame<TPixel> destination = new(
quantizer.Configuration,
interest.Width,
interest.Height,
@ -99,13 +99,39 @@ public static class QuantizerUtilities
return destination;
}
internal static void BuildPalette<TPixel>(
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The pixel specific quantizer.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image to sample from.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
}
}
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The pixel specific quantizer.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image frame to sample from.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> image)
ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(image))
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
}

221
tests/ImageSharp.Tests/Drawing/DrawImageTests.cs

@ -11,42 +11,40 @@ namespace SixLabors.ImageSharp.Tests.Drawing;
[GroupOutput("Drawing")]
public class DrawImageTests
{
public static readonly TheoryData<PixelColorBlendingMode> BlendingModes = new TheoryData<PixelColorBlendingMode>
{
PixelColorBlendingMode.Normal,
PixelColorBlendingMode.Multiply,
PixelColorBlendingMode.Add,
PixelColorBlendingMode.Subtract,
PixelColorBlendingMode.Screen,
PixelColorBlendingMode.Darken,
PixelColorBlendingMode.Lighten,
PixelColorBlendingMode.Overlay,
PixelColorBlendingMode.HardLight,
};
public static readonly TheoryData<PixelColorBlendingMode> BlendingModes = new()
{
PixelColorBlendingMode.Normal,
PixelColorBlendingMode.Multiply,
PixelColorBlendingMode.Add,
PixelColorBlendingMode.Subtract,
PixelColorBlendingMode.Screen,
PixelColorBlendingMode.Darken,
PixelColorBlendingMode.Lighten,
PixelColorBlendingMode.Overlay,
PixelColorBlendingMode.HardLight,
};
[Theory]
[WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)]
public void ImageBlendingMatchesSvgSpecExamples<TPixel>(TestImageProvider<TPixel> provider, PixelColorBlendingMode mode)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> background = provider.GetImage())
using (var source = Image.Load<TPixel>(TestFile.Create(TestImages.Png.Ducky).Bytes))
{
background.Mutate(x => x.DrawImage(source, mode, 1F));
background.DebugSave(
provider,
new { mode = mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
var comparer = ImageComparer.TolerantPercentage(0.01F);
background.CompareToReferenceOutput(
comparer,
provider,
new { mode = mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
using Image<TPixel> background = provider.GetImage();
using Image<TPixel> source = Image.Load<TPixel>(TestFile.Create(TestImages.Png.Ducky).Bytes);
background.Mutate(x => x.DrawImage(source, mode, 1F));
background.DebugSave(
provider,
new { mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
ImageComparer comparer = ImageComparer.TolerantPercentage(0.01F);
background.CompareToReferenceOutput(
comparer,
provider,
new { mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
@ -68,28 +66,29 @@ public class DrawImageTests
float opacity)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
using (var blend = Image.Load<TPixel>(TestFile.Create(brushImage).Bytes))
using Image<TPixel> image = provider.GetImage();
using Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(brushImage).Bytes);
Size size = new(image.Width * 3 / 4, image.Height * 3 / 4);
Point position = new(image.Width / 8, image.Height / 8);
blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic));
image.Mutate(x => x.DrawImage(blend, position, mode, opacity));
FormattableString testInfo = $"{Path.GetFileNameWithoutExtension(brushImage)}-{mode}-{opacity}";
PngEncoder encoder;
if (provider.PixelType == PixelTypes.Rgba64)
{
var size = new Size(image.Width * 3 / 4, image.Height * 3 / 4);
var position = new Point(image.Width / 8, image.Height / 8);
blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic));
image.Mutate(x => x.DrawImage(blend, position, mode, opacity));
FormattableString testInfo = $"{System.IO.Path.GetFileNameWithoutExtension(brushImage)}-{mode}-{opacity}";
var encoder = new PngEncoder();
if (provider.PixelType == PixelTypes.Rgba64)
{
encoder.BitDepth = PngBitDepth.Bit16;
}
image.DebugSave(provider, testInfo, encoder: encoder);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.01f),
provider,
testInfo);
encoder = new() { BitDepth = PngBitDepth.Bit16 };
}
else
{
encoder = new();
}
image.DebugSave(provider, testInfo, encoder: encoder);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.01f),
provider,
testInfo);
}
[Theory]
@ -99,19 +98,17 @@ public class DrawImageTests
{
byte[] brushData = TestFile.Create(TestImages.Png.Ducky).Bytes;
using (Image<TPixel> image = provider.GetImage())
using (Image brushImage = provider.PixelType == PixelTypes.Rgba32
? (Image)Image.Load<Bgra32>(brushData)
: Image.Load<Rgba32>(brushData))
{
image.Mutate(c => c.DrawImage(brushImage, 0.5f));
image.DebugSave(provider, appendSourceFileOrDescription: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.01f),
provider,
appendSourceFileOrDescription: false);
}
using Image<TPixel> image = provider.GetImage();
using Image brushImage = provider.PixelType == PixelTypes.Rgba32
? Image.Load<Bgra32>(brushData)
: Image.Load<Rgba32>(brushData);
image.Mutate(c => c.DrawImage(brushImage, 0.5f));
image.DebugSave(provider, appendSourceFileOrDescription: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.01f),
provider,
appendSourceFileOrDescription: false);
}
[Theory]
@ -121,26 +118,24 @@ public class DrawImageTests
[WithSolidFilledImages(100, 100, "White", PixelTypes.Rgba32, -25, -30)]
public void WorksWithDifferentLocations(TestImageProvider<Rgba32> provider, int x, int y)
{
using (Image<Rgba32> background = provider.GetImage())
using (var overlay = new Image<Rgba32>(50, 50))
{
Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> overlayMem));
overlayMem.Span.Fill(Color.Black);
background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F));
background.DebugSave(
provider,
testOutputDetails: $"{x}_{y}",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
provider,
testOutputDetails: $"{x}_{y}",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
using Image<Rgba32> background = provider.GetImage();
using Image<Rgba32> overlay = new(50, 50);
Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> overlayMem));
overlayMem.Span.Fill(Color.Black);
background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F));
background.DebugSave(
provider,
testOutputDetails: $"{x}_{y}",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
provider,
testOutputDetails: $"{x}_{y}",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
@ -148,29 +143,27 @@ public class DrawImageTests
public void DrawTransformed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(45F)
.AppendScale(new SizeF(.25F, .25F))
.AppendTranslation(new PointF(10, 10));
// Apply a background color so we can see the translation.
blend.Mutate(x => x.Transform(builder));
blend.Mutate(x => x.BackgroundColor(Color.HotPink));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, position, .75F));
image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.002f),
provider,
appendSourceFileOrDescription: false,
appendPixelTypeToFileName: false);
}
using Image<TPixel> image = provider.GetImage();
using Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes);
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(45F)
.AppendScale(new SizeF(.25F, .25F))
.AppendTranslation(new PointF(10, 10));
// Apply a background color so we can see the translation.
blend.Mutate(x => x.Transform(builder));
blend.Mutate(x => x.BackgroundColor(Color.HotPink));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
Point position = new((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, position, .75F));
image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.002f),
provider,
appendSourceFileOrDescription: false,
appendPixelTypeToFileName: false);
}
[Theory]
@ -180,17 +173,15 @@ public class DrawImageTests
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, 130)]
public void NonOverlappingImageThrows(TestImageProvider<Rgba32> provider, int x, int y)
{
using (Image<Rgba32> background = provider.GetImage())
using (var overlay = new Image<Rgba32>(Configuration.Default, 10, 10, Color.Black))
{
ImageProcessingException ex = Assert.Throws<ImageProcessingException>(Test);
using Image<Rgba32> background = provider.GetImage();
using Image<Rgba32> overlay = new(Configuration.Default, 10, 10, Color.Black);
ImageProcessingException ex = Assert.Throws<ImageProcessingException>(Test);
Assert.Contains("does not overlap", ex.ToString());
Assert.Contains("does not overlap", ex.ToString());
void Test()
{
background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions()));
}
void Test()
{
background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions()));
}
}
}

166
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -46,7 +46,7 @@ public class GifEncoderTests
using (Image<TPixel> image = provider.GetImage())
{
var encoder = new GifEncoder
GifEncoder encoder = new()
{
// Use the palette quantizer without dithering to ensure results
// are consistent
@ -59,59 +59,45 @@ public class GifEncoderTests
// Compare encoded result
string path = provider.Utility.GetTestOutputFileName("gif", null, true);
using (var encoded = Image.Load<Rgba32>(path))
{
encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif");
}
using Image<Rgba32> encoded = Image.Load<Rgba32>(path);
encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif");
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var options = new GifEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
GifEncoder options = new();
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, options);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
[Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten()
{
var options = new GifEncoder();
GifEncoder options = new();
var testFile = TestFile.Create(TestImages.Gif.Rings);
TestFile testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
GifMetadata metadata = output.Metadata.GetGifMetadata();
Assert.Equal(1, metadata.Comments.Count);
Assert.Equal("ImageSharp", metadata.Comments[0]);
}
}
}
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, options);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
GifMetadata metadata = output.Metadata.GetGifMetadata();
Assert.Equal(1, metadata.Comments.Count);
Assert.Equal("ImageSharp", metadata.Comments[0]);
}
[Theory]
@ -119,25 +105,28 @@ public class GifEncoderTests
public void EncodeGlobalPaletteReturnsSmallerFile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
using Image<TPixel> image = provider.GetImage();
GifEncoder encoder = new()
{
var encoder = new GifEncoder
{
ColorTableMode = GifColorTableMode.Global,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null })
};
ColorTableMode = GifColorTableMode.Global,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null })
};
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global");
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global");
encoder.ColorTableMode = GifColorTableMode.Local;
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local");
encoder = new()
{
ColorTableMode = GifColorTableMode.Local,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }),
};
var fileInfoGlobal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "global"));
var fileInfoLocal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "local"));
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local");
Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length);
}
FileInfo fileInfoGlobal = new(provider.Utility.GetTestOutputFileName("gif", "global"));
FileInfo fileInfoLocal = new(provider.Utility.GetTestOutputFileName("gif", "local"));
Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length);
}
[Theory]
@ -152,10 +141,10 @@ public class GifEncoderTests
{
using Image<TPixel> image = provider.GetImage();
var encoder = new GifEncoder()
GifEncoder encoder = new()
{
ColorTableMode = GifColorTableMode.Global,
GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio)
PixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio)
};
string testOutputFile = provider.Utility.SaveTestOutputFile(
@ -166,8 +155,7 @@ public class GifEncoderTests
appendPixelTypeToFileName: false);
// TODO: For proper regression testing of gifs, use a multi-frame reference output, or find a working reference decoder.
// IImageDecoder referenceDecoder = TestEnvironment.Ge
// ReferenceDecoder(testOutputFile);
// IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(testOutputFile);
// using var encoded = Image.Load<TPixel>(testOutputFile, referenceDecoder);
// ValidatorComparer.VerifySimilarity(image, encoded);
}
@ -175,44 +163,42 @@ public class GifEncoderTests
[Fact]
public void NonMutatingEncodePreservesPaletteCount()
{
using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes))
using (var outStream = new MemoryStream())
using MemoryStream inStream = new(TestFile.Create(TestImages.Gif.Leo).Bytes);
using MemoryStream outStream = new();
inStream.Position = 0;
Image<Rgba32> image = Image.Load<Rgba32>(inStream);
GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode;
GifEncoder encoder = new()
{
inStream.Position = 0;
var image = Image.Load<Rgba32>(inStream);
GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode;
var encoder = new GifEncoder
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
};
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
};
image.Save(outStream, encoder);
outStream.Position = 0;
image.Save(outStream, encoder);
outStream.Position = 0;
outStream.Position = 0;
var clone = Image.Load<Rgba32>(outStream);
outStream.Position = 0;
Image<Rgba32> clone = Image.Load<Rgba32>(outStream);
GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata();
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata();
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetadata.ColorTableLength);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetadata.ColorTableLength);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
}
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
image.Dispose();
clone.Dispose();
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
}
image.Dispose();
clone.Dispose();
}
}

90
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs

@ -15,9 +15,9 @@ public partial class PngEncoderTests
public void HeaderChunk_ComesFirst()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, PngEncoder);
@ -25,8 +25,8 @@ public partial class PngEncoderTests
// assert
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.Equal(PngChunkType.Header, type);
}
@ -34,9 +34,9 @@ public partial class PngEncoderTests
public void EndChunk_IsLast()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, PngEncoder);
@ -47,15 +47,15 @@ public partial class PngEncoderTests
bool endChunkFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(endChunkFound);
if (type == PngChunkType.End)
{
endChunkFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
}
@ -68,10 +68,10 @@ public partial class PngEncoderTests
public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
{
// arrange
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
PngChunkType chunkType = (PngChunkType)chunkTypeObj;
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, PngEncoder);
@ -83,8 +83,8 @@ public partial class PngEncoderTests
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
@ -100,7 +100,7 @@ public partial class PngEncoderTests
break;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
}
@ -110,10 +110,10 @@ public partial class PngEncoderTests
public void Chunk_ComesBeforeIDat(object chunkTypeObj)
{
// arrange
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
PngChunkType chunkType = (PngChunkType)chunkTypeObj;
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, PngEncoder);
@ -124,8 +124,8 @@ public partial class PngEncoderTests
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
@ -136,7 +136,7 @@ public partial class PngEncoderTests
dataFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
}
@ -144,18 +144,18 @@ public partial class PngEncoderTests
public void IgnoreMetadata_WillExcludeAllAncillaryChunks()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
using MemoryStream memStream = new();
PngEncoder encoder = new() { SkipMetadata = true, TextCompressionThreshold = 8 };
Dictionary<PngChunkType, bool> expectedChunkTypes = new()
{
{ PngChunkType.Header, false },
{ PngChunkType.Palette, false },
{ PngChunkType.Data, false },
{ PngChunkType.End, false }
};
var excludedChunkTypes = new List<PngChunkType>()
List<PngChunkType> excludedChunkTypes = new()
{
PngChunkType.Gamma,
PngChunkType.Exif,
@ -174,15 +174,15 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType))
{
expectedChunkTypes[chunkType] = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
// all expected chunk types should have been seen at least once.
@ -201,12 +201,12 @@ public partial class PngEncoderTests
public void ExcludeFilter_Works(object filterObj)
{
// arrange
var chunkFilter = (PngChunkFilter)filterObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
PngChunkFilter chunkFilter = (PngChunkFilter)filterObj;
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
using MemoryStream memStream = new();
PngEncoder encoder = new() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
Dictionary<PngChunkType, bool> expectedChunkTypes = new()
{
{ PngChunkType.Header, false },
{ PngChunkType.Gamma, false },
@ -219,7 +219,7 @@ public partial class PngEncoderTests
{ PngChunkType.Data, false },
{ PngChunkType.End, false }
};
var excludedChunkTypes = new List<PngChunkType>();
List<PngChunkType> excludedChunkTypes = new();
switch (chunkFilter)
{
case PngChunkFilter.ExcludeGammaChunk:
@ -267,15 +267,15 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType))
{
expectedChunkTypes[chunkType] = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
// all expected chunk types should have been seen at least once.
@ -289,11 +289,11 @@ public partial class PngEncoderTests
public void ExcludeFilter_WithNone_DoesNotExcludeChunks()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
var expectedChunkTypes = new List<PngChunkType>()
using MemoryStream memStream = new();
PngEncoder encoder = new() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
List<PngChunkType> expectedChunkTypes = new()
{
PngChunkType.Header,
PngChunkType.Gamma,
@ -314,11 +314,11 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.True(expectedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been present");
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
bytesSpan = bytesSpan[(4 + 4 + length + 4)..];
}
}
}

23
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs

@ -3,24 +3,21 @@
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Writers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
[Trait("Format", "Tiff")]
public class TiffEncoderHeaderTests
{
private static readonly MemoryAllocator MemoryAllocator = MemoryAllocator.Create();
private static readonly Configuration Configuration = Configuration.Default;
private static readonly ITiffEncoderOptions Options = new TiffEncoder();
private static readonly TiffEncoder Encoder = new();
[Fact]
public void WriteHeader_WritesValidHeader()
{
using var stream = new MemoryStream();
var encoder = new TiffEncoderCore(Options, MemoryAllocator);
using MemoryStream stream = new();
TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator);
using (var writer = new TiffStreamWriter(stream))
using (TiffStreamWriter writer = new(stream))
{
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer);
}
@ -31,13 +28,11 @@ public class TiffEncoderHeaderTests
[Fact]
public void WriteHeader_ReturnsFirstIfdMarker()
{
using var stream = new MemoryStream();
var encoder = new TiffEncoderCore(Options, MemoryAllocator);
using MemoryStream stream = new();
TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator);
using (var writer = new TiffStreamWriter(stream))
{
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer);
Assert.Equal(4, firstIfdMarker);
}
using TiffStreamWriter writer = new(stream);
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer);
Assert.Equal(4, firstIfdMarker);
}
}

142
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs

@ -14,9 +14,7 @@ public abstract partial class ImageFrameCollectionTests
{
[Fact]
public void Constructor_ShouldCreateOneFrame()
{
Assert.Equal(1, this.Collection.Count);
}
=> Assert.Equal(1, this.Collection.Count);
[Fact]
public void AddNewFrame_FramesMustHaveSameSize()
@ -24,7 +22,7 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>(
() =>
{
using var frame = new ImageFrame<Rgba32>(Configuration.Default, 1, 1);
using ImageFrame<Rgba32> frame = new(Configuration.Default, 1, 1);
using ImageFrame<Rgba32> addedFrame = this.Collection.AddFrame(frame);
});
@ -75,7 +73,7 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>(
() =>
{
using var frame = new ImageFrame<Rgba32>(Configuration.Default, 1, 1);
using ImageFrame<Rgba32> frame = new(Configuration.Default, 1, 1);
using ImageFrame<Rgba32> insertedFrame = this.Collection.InsertFrame(1, frame);
});
@ -100,8 +98,8 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>(
() =>
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 1, 1);
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 1, 1);
new ImageFrameCollection<Rgba32>(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -113,8 +111,8 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void RemoveAtFrame_ThrowIfRemovingLastFrame()
{
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame });
@ -126,9 +124,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist()
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -139,9 +137,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void RootFrameIsFrameAtIndexZero()
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -151,9 +149,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void ConstructorPopulatesFrames()
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -163,9 +161,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void DisposeClearsCollection()
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -177,9 +175,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void Dispose_DisposesAllInnerFrames()
{
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>(
using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> collection = new(
this.Image,
new[] { imageFrame1, imageFrame2 });
@ -188,11 +186,8 @@ public abstract partial class ImageFrameCollectionTests
Assert.All(
framesSnapShot,
f =>
{
// The pixel source of the frame is null after its been disposed.
Assert.Null(f.PixelBuffer);
});
f => // The pixel source of the frame is null after its been disposed.
Assert.Null(f.PixelBuffer));
}
[Theory]
@ -200,18 +195,14 @@ public abstract partial class ImageFrameCollectionTests
public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway
using (Image<TPixel> cloned = img.Frames.CloneFrame(0))
{
Assert.Equal(2, img.Frames.Count);
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
cloned.ComparePixelBufferTo(imgMem);
}
}
using Image<TPixel> img = provider.GetImage();
using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway
using Image<TPixel> cloned = img.Frames.CloneFrame(0);
Assert.Equal(2, img.Frames.Count);
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
cloned.ComparePixelBufferTo(imgMem);
}
[Theory]
@ -219,19 +210,15 @@ public abstract partial class ImageFrameCollectionTests
public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMemory));
TPixel[] sourcePixelData = imgMemory.ToArray();
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame);
using (Image<TPixel> cloned = img.Frames.ExportFrame(0))
{
Assert.Equal(1, img.Frames.Count);
cloned.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
}
using Image<TPixel> img = provider.GetImage();
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMemory));
TPixel[] sourcePixelData = imgMemory.ToArray();
using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame);
using Image<TPixel> cloned = img.Frames.ExportFrame(0);
Assert.Equal(1, img.Frames.Count);
cloned.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
[Fact]
@ -266,7 +253,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void AddFrame_clones_sourceFrame()
{
using var otherFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> otherFrame = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> addedFrame = this.Image.Frames.AddFrame(otherFrame);
Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem));
@ -277,7 +264,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void InsertFrame_clones_sourceFrame()
{
using var otherFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> otherFrame = new(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> addedFrame = this.Image.Frames.InsertFrame(0, otherFrame);
Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem));
@ -332,7 +319,7 @@ public abstract partial class ImageFrameCollectionTests
this.Image.Frames.CreateFrame();
}
using var frame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
using ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
Assert.False(this.Image.Frames.Contains(frame));
}
@ -343,14 +330,13 @@ public abstract partial class ImageFrameCollectionTests
configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = 1000 };
configuration.PreferContiguousImageBuffers = true;
using var image = new Image<Rgba32>(configuration, 100, 100);
using Image<Rgba32> image = new(configuration, 100, 100);
image.Frames.CreateFrame();
image.Frames.InsertFrame(0, image.Frames[0]);
image.Frames.CreateFrame(Color.Red);
Assert.Equal(4, image.Frames.Count);
IEnumerable<ImageFrame<Rgba32>> frames = image.Frames;
foreach (ImageFrame<Rgba32> frame in frames)
foreach (ImageFrame<Rgba32> frame in image.Frames)
{
Assert.True(frame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> _));
}
@ -359,8 +345,8 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void DisposeCall_NoThrowIfCalledMultiple()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
Image<Rgba32> image = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well
frameCollection.Dispose();
@ -369,33 +355,33 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void PublicProperties_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
Image<Rgba32> image = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.RootFrame; });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame prop = frameCollection.RootFrame; });
}
[Fact]
public void PublicMethods_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
Image<Rgba32> image = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(default(ImageFrame<Rgba32>)); });
Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { bool res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { IEnumerator<ImageFrame<Rgba32>> res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { int prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => frameCollection.RemoveFrame(default));
Assert.Throws<ObjectDisposedException>(() => frameCollection.MoveFrame(default, default));
}
}
}

194
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs

@ -19,7 +19,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void AddFrame_OfDifferentPixelType()
{
using (var sourceImage = new Image<Bgra32>(
using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(),
this.Image.Width,
this.Image.Height,
@ -32,7 +32,7 @@ public abstract partial class ImageFrameCollectionTests
Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray();
Assert.Equal(2, this.Collection.Count);
var actualFrame = (ImageFrame<Rgba32>)this.Collection[1];
ImageFrame<Rgba32> actualFrame = (ImageFrame<Rgba32>)this.Collection[1];
actualFrame.ComparePixelBufferTo(expectedAllBlue);
}
@ -40,7 +40,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void InsertFrame_OfDifferentPixelType()
{
using (var sourceImage = new Image<Bgra32>(
using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(),
this.Image.Width,
this.Image.Height,
@ -53,25 +53,20 @@ public abstract partial class ImageFrameCollectionTests
Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray();
Assert.Equal(2, this.Collection.Count);
var actualFrame = (ImageFrame<Rgba32>)this.Collection[0];
ImageFrame<Rgba32> actualFrame = (ImageFrame<Rgba32>)this.Collection[0];
actualFrame.ComparePixelBufferTo(expectedAllBlue);
}
[Fact]
public void Constructor_ShouldCreateOneFrame()
{
Assert.Equal(1, this.Collection.Count);
}
=> Assert.Equal(1, this.Collection.Count);
[Fact]
public void AddNewFrame_FramesMustHaveSameSize()
{
ArgumentException ex = Assert.Throws<ArgumentException>(
() =>
{
this.Collection.AddFrame(new ImageFrame<Rgba32>(Configuration.Default, 1, 1));
});
() => this.Collection.AddFrame(new ImageFrame<Rgba32>(Configuration.Default, 1, 1)));
Assert.StartsWith("Frame must have the same dimensions as the image.", ex.Message);
}
@ -80,10 +75,7 @@ public abstract partial class ImageFrameCollectionTests
public void AddNewFrame_Frame_FramesNotBeNull()
{
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
() =>
{
this.Collection.AddFrame(null);
});
() => this.Collection.AddFrame(null));
Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
}
@ -92,10 +84,7 @@ public abstract partial class ImageFrameCollectionTests
public void InsertNewFrame_FramesMustHaveSameSize()
{
ArgumentException ex = Assert.Throws<ArgumentException>(
() =>
{
this.Collection.InsertFrame(1, new ImageFrame<Rgba32>(Configuration.Default, 1, 1));
});
() => this.Collection.InsertFrame(1, new ImageFrame<Rgba32>(Configuration.Default, 1, 1)));
Assert.StartsWith("Frame must have the same dimensions as the image.", ex.Message);
}
@ -104,10 +93,7 @@ public abstract partial class ImageFrameCollectionTests
public void InsertNewFrame_FramesNotBeNull()
{
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
() =>
{
this.Collection.InsertFrame(1, null);
});
() => this.Collection.InsertFrame(1, null));
Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
}
@ -116,10 +102,7 @@ public abstract partial class ImageFrameCollectionTests
public void RemoveAtFrame_ThrowIfRemovingLastFrame()
{
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
() =>
{
this.Collection.RemoveFrame(0);
});
() => this.Collection.RemoveFrame(0));
Assert.Equal("Cannot remove last frame.", ex.Message);
}
@ -134,30 +117,24 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void RootFrameIsFrameAtIndexZero()
{
Assert.Equal(this.Collection.RootFrame, this.Collection[0]);
}
=> Assert.Equal(this.Collection.RootFrame, this.Collection[0]);
[Theory]
[WithTestPatternImages(10, 10, PixelTypes.Rgba32 | PixelTypes.Bgr24)]
public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
ImageFrameCollection nonGenericFrameCollection = img.Frames;
using Image<TPixel> img = provider.GetImage();
ImageFrameCollection nonGenericFrameCollection = img.Frames;
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10)); // add a frame anyway
using (Image cloned = nonGenericFrameCollection.CloneFrame(0))
{
Assert.Equal(2, img.Frames.Count);
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10)); // add a frame anyway
using Image cloned = nonGenericFrameCollection.CloneFrame(0);
Assert.Equal(2, img.Frames.Count);
var expectedClone = (Image<TPixel>)cloned;
Image<TPixel> expectedClone = (Image<TPixel>)cloned;
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
expectedClone.ComparePixelBufferTo(imgMem);
}
}
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
expectedClone.ComparePixelBufferTo(imgMem);
}
[Theory]
@ -165,22 +142,18 @@ public abstract partial class ImageFrameCollectionTests
public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
TPixel[] sourcePixelData = imgMem.ToArray();
using Image<TPixel> img = provider.GetImage();
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
TPixel[] sourcePixelData = imgMem.ToArray();
ImageFrameCollection nonGenericFrameCollection = img.Frames;
ImageFrameCollection nonGenericFrameCollection = img.Frames;
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10));
using (Image cloned = nonGenericFrameCollection.ExportFrame(0))
{
Assert.Equal(1, img.Frames.Count);
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10));
using Image cloned = nonGenericFrameCollection.ExportFrame(0);
Assert.Equal(1, img.Frames.Count);
var expectedClone = (Image<TPixel>)cloned;
expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
}
Image<TPixel> expectedClone = (Image<TPixel>)cloned;
expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
[Fact]
@ -190,7 +163,7 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(2, this.Image.Frames.Count);
var frame = (ImageFrame<Rgba32>)this.Image.Frames[1];
ImageFrame<Rgba32> frame = (ImageFrame<Rgba32>)this.Image.Frames[1];
frame.ComparePixelBufferTo(default(Rgba32));
}
@ -202,7 +175,7 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(2, this.Image.Frames.Count);
var frame = (ImageFrame<Rgba32>)this.Image.Frames[1];
ImageFrame<Rgba32> frame = (ImageFrame<Rgba32>)this.Image.Frames[1];
frame.ComparePixelBufferTo(Color.HotPink);
}
@ -210,132 +183,127 @@ public abstract partial class ImageFrameCollectionTests
[Fact]
public void MoveFrame_LeavesFrameInCorrectLocation()
{
for (var i = 0; i < 9; i++)
for (int i = 0; i < 9; i++)
{
this.Image.Frames.CreateFrame();
}
var frame = this.Image.Frames[4];
ImageFrame frame = this.Image.Frames[4];
this.Image.Frames.MoveFrame(4, 7);
var newIndex = this.Image.Frames.IndexOf(frame);
int newIndex = this.Image.Frames.IndexOf(frame);
Assert.Equal(7, newIndex);
}
[Fact]
public void IndexOf_ReturnsCorrectIndex()
{
for (var i = 0; i < 9; i++)
for (int i = 0; i < 9; i++)
{
this.Image.Frames.CreateFrame();
}
var frame = this.Image.Frames[4];
var index = this.Image.Frames.IndexOf(frame);
ImageFrame frame = this.Image.Frames[4];
int index = this.Image.Frames.IndexOf(frame);
Assert.Equal(4, index);
}
[Fact]
public void Contains_TrueIfMember()
{
for (var i = 0; i < 9; i++)
for (int i = 0; i < 9; i++)
{
this.Image.Frames.CreateFrame();
}
var frame = this.Image.Frames[4];
ImageFrame frame = this.Image.Frames[4];
Assert.True(this.Image.Frames.Contains(frame));
}
[Fact]
public void Contains_FalseIfNonMember()
{
for (var i = 0; i < 9; i++)
for (int i = 0; i < 9; i++)
{
this.Image.Frames.CreateFrame();
}
var frame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
Assert.False(this.Image.Frames.Contains(frame));
}
[Fact]
public void PublicProperties_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames;
Image<Rgba32> image = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.RootFrame; });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> prop = frameCollection.RootFrame; });
}
[Fact]
public void PublicMethods_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames;
var rgba32Array = new Rgba32[0];
Image<Rgba32> image = new(Configuration.Default, 10, 10);
ImageFrameCollection<Rgba32> frameCollection = image.Frames;
Rgba32[] rgba32Array = Array.Empty<Rgba32>();
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame<Rgba32>)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array.AsSpan()); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame res = frameCollection.AddFrame((ImageFrame)null); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(rgba32Array); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame((ImageFrame<Rgba32>)null); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(rgba32Array.AsSpan()); });
Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { bool res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { IEnumerator<ImageFrame<Rgba32>> res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { int prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => frameCollection.RemoveFrame(default));
Assert.Throws<ObjectDisposedException>(() => frameCollection.MoveFrame(default, default));
}
/// <summary>
/// Integration test for end-to end API validation.
/// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="provider">The test image provider</param>
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void ConstructGif_FromDifferentPixelTypes<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image source = provider.GetImage())
using (var dest = new Image<TPixel>(source.GetConfiguration(), source.Width, source.Height))
using Image source = provider.GetImage();
using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height);
// Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
ImportFrameAs<Rgba64>(source.Frames, dest.Frames, 2);
ImportFrameAs<Rgba32>(source.Frames, dest.Frames, 3);
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 4);
// Drop the original empty root frame:
dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif");
dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++)
{
// Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
ImportFrameAs<Rgba64>(source.Frames, dest.Frames, 2);
ImportFrameAs<Rgba32>(source.Frames, dest.Frames, 3);
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 4);
// Drop the original empty root frame:
dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif");
dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++)
{
CompareGifMetadata(source.Frames[i], dest.Frames[i]);
}
CompareGifMetadata(source.Frames[i], dest.Frames[i]);
}
}
private static void ImportFrameAs<TPixel>(ImageFrameCollection source, ImageFrameCollection destination, int index)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image temp = source.CloneFrame(index))
{
using (Image<TPixel> temp2 = temp.CloneAs<TPixel>())
{
destination.AddFrame(temp2.Frames.RootFrame);
}
}
using Image temp = source.CloneFrame(index);
using Image<TPixel> temp2 = temp.CloneAs<TPixel>();
destination.AddFrame(temp2.Frames.RootFrame);
}
private static void CompareGifMetadata(ImageFrame a, ImageFrame b)

100
tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs

@ -10,24 +10,36 @@ namespace SixLabors.ImageSharp.Tests.Quantization;
public class PixelSamplingStrategyTests
{
public static readonly TheoryData<int, int, int, int> DefaultPixelSamplingStrategy_Data = new TheoryData<int, int, int, int>()
{
{ 100, 100, 1, 10000 },
{ 100, 100, 1, 5000 },
{ 100, 100, 10, 50000 },
{ 99, 100, 11, 30000 },
{ 97, 99, 11, 80000 },
{ 99, 100, 11, 20000 },
{ 99, 501, 20, 100000 },
{ 97, 500, 20, 10000 },
{ 103, 501, 20, 1000 },
};
public static readonly TheoryData<int, int, int, int> DefaultPixelSamplingStrategy_MultiFrame_Data = new()
{
{ 100, 100, 1, 10000 },
{ 100, 100, 1, 5000 },
{ 100, 100, 10, 50000 },
{ 99, 100, 11, 30000 },
{ 97, 99, 11, 80000 },
{ 99, 100, 11, 20000 },
{ 99, 501, 20, 100000 },
{ 97, 500, 20, 10000 },
{ 103, 501, 20, 1000 },
};
public static readonly TheoryData<int, int, int> DefaultPixelSamplingStrategy_Data = new()
{
{ 100, 100, 9900 },
{ 100, 100, 5000 },
{ 99, 100, 30000 },
{ 97, 99, 80000 },
{ 99, 100, 20000 },
{ 99, 501, 100000 },
{ 97, 500, 10000 },
{ 103, 501, 1000 },
};
[Fact]
public void ExtensivePixelSamplingStrategy_EnumeratesAll()
public void ExtensivePixelSamplingStrategy_EnumeratesAll_MultiFrame()
{
using Image<L8> image = CreateTestImage(100, 100, 100);
var strategy = new ExtensivePixelSamplingStrategy();
ExtensivePixelSamplingStrategy strategy = new();
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image))
{
@ -39,13 +51,32 @@ public class PixelSamplingStrategyTests
ImageComparer.Exact.VerifySimilarity(expected, image);
}
[Fact]
public void ExtensivePixelSamplingStrategy_EnumeratesAll()
{
using Image<L8> image = CreateTestImage(100, 100, 100);
ExtensivePixelSamplingStrategy strategy = new();
foreach (ImageFrame<L8> frame in image.Frames)
{
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(frame))
{
PaintWhite(region);
}
}
using Image<L8> expected = CreateTestImage(100, 100, 100, true);
ImageComparer.Exact.VerifySimilarity(expected, image);
}
[Theory]
[WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)]
public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider<L8> dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels)
[WithBlankImages(nameof(DefaultPixelSamplingStrategy_MultiFrame_Data), 1, 1, PixelTypes.L8)]
public void DefaultPixelSamplingStrategy_IsFair_MultiFrame(TestImageProvider<L8> dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels)
{
using Image<L8> image = CreateTestImage(width, height, noOfFrames);
var strategy = new DefaultPixelSamplingStrategy(maximumNumberOfPixels, 0.1);
DefaultPixelSamplingStrategy strategy = new(maximumNumberOfPixels, 0.1);
long visitedPixels = 0;
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image))
@ -67,9 +98,40 @@ public class PixelSamplingStrategyTests
Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}");
}
[Theory]
[WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)]
public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider<L8> dummyProvider, int width, int height, int maximumNumberOfPixels)
{
using Image<L8> image = CreateTestImage(width, height, 1);
DefaultPixelSamplingStrategy strategy = new(maximumNumberOfPixels, 0.1);
long visitedPixels = 0;
foreach (ImageFrame<L8> frame in image.Frames)
{
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(frame))
{
PaintWhite(region);
visitedPixels += region.Width * region.Height;
}
}
image.DebugSave(
dummyProvider,
$"W{width}_H{height}_maximumNumberOfPixels_{maximumNumberOfPixels}",
appendPixelTypeToFileName: false);
int maximumPixels = image.Width * image.Height * image.Frames.Count / 10;
maximumPixels = Math.Max(maximumPixels, (int)strategy.MaximumPixels);
// allow some inaccuracy:
double visitRatio = visitedPixels / (double)maximumPixels;
Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}");
}
private static void PaintWhite(Buffer2DRegion<L8> region)
{
var white = new L8(255);
L8 white = new(255);
for (int y = 0; y < region.Height; y++)
{
region.DangerousGetRowSpan(y).Fill(white);
@ -79,7 +141,7 @@ public class PixelSamplingStrategyTests
private static Image<L8> CreateTestImage(int width, int height, int noOfFrames, bool paintWhite = false)
{
L8 bg = paintWhite ? new L8(255) : default;
var image = new Image<L8>(width, height, bg);
Image<L8> image = new(width, height, bg);
for (int i = 1; i < noOfFrames; i++)
{

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

@ -4,8 +4,6 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@ -13,57 +11,20 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
/// A Png encoder that uses the ImageSharp core encoder but the default configuration.
/// This allows encoding under environments with restricted memory.
/// </summary>
public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder, IPngEncoderOptions
public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder
{
/// <inheritdoc/>
public PngBitDepth? BitDepth { get; set; }
/// <inheritdoc/>
public PngColorType? ColorType { get; set; }
/// <inheritdoc/>
public PngFilterMethod? FilterMethod { get; set; }
/// <inheritdoc/>
public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression;
/// <inheritdoc/>
public int TextCompressionThreshold { get; set; } = 1024;
/// <inheritdoc/>
public float? Gamma { get; set; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public byte Threshold { get; set; } = byte.MaxValue;
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
/// <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>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
{
Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator;
using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this));
using PngEncoderCore encoder = new(allocator, configuration, this);
encoder.Encode(image, stream);
}
@ -75,8 +36,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder
/// <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 async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
public override async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator;
@ -84,7 +44,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder
// 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 var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this));
using PngEncoderCore encoder = new(allocator, configuration, this);
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
}
}

Loading…
Cancel
Save