Browse Source

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

Normalize and cleanup encoders
pull/2237/merge
James Jackson-South 4 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 /// 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!!! /// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!!
/// </remarks> /// </remarks>
/// <exception cref="InvalidOperationException">
/// This method is used for AOT code generation only. Do not call it at runtime.
/// </exception>
[Preserve] [Preserve]
private static void SeedPixelFormats() private static void SeedPixelFormats()
{ {
@ -487,8 +490,10 @@ internal static class AotCompilerTools
private static void AotCompilePixelSamplingStrategys<TPixel>() private static void AotCompilePixelSamplingStrategys<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
default(DefaultPixelSamplingStrategy).EnumeratePixelRegions<TPixel>(default); default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(Image<TPixel>));
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions<TPixel>(default); default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(Image<TPixel>));
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
} }
/// <summary> /// <summary>
@ -513,13 +518,13 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TDither : struct, IDither where TDither : struct, IDither
{ {
var octree = default(OctreeQuantizer<TPixel>); OctreeQuantizer<TPixel> octree = default;
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, 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); 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).ApplyQuantizationDither<WuQuantizer<TPixel>, TPixel>(ref wu, default, default, default);
default(TDither).ApplyPaletteDither<PaletteDitherProcessor<TPixel>.DitherProcessor, TPixel>(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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a Windows bitmap. /// Image encoder for writing an image to a stream as a Windows bitmap.
/// </summary> /// </summary>
public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions public sealed class BmpEncoder : QuantizingImageEncoder
{ {
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel. /// Gets the number of bits per pixel.
/// </summary> /// </summary>
public BmpBitsPerPixel? BitsPerPixel { get; set; } public BmpBitsPerPixel? BitsPerPixel { get; init; }
/// <summary> /// <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 /// 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. /// 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. /// Instead a bitmap version 4 info header will be written with the BITFIELDS compression.
/// </summary> /// </summary>
public bool SupportTransparency { get; set; } public bool SupportTransparency { get; init; }
/// <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; }
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken); 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.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;
@ -92,17 +91,23 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private readonly IQuantizer quantizer;
/// <summary>
/// The pixel sampling strategy for quantization.
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class. /// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The encoder options.</param> /// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param> /// <param name="memoryAllocator">The memory manager.</param>
public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocator) public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel; this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.quantizer = encoder.Quantizer;
this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
} }
/// <summary> /// <summary>
@ -159,7 +164,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame); this.WriteImage(stream, image);
WriteColorProfile(stream, iccProfileData, buffer); WriteColorProfile(stream, iccProfileData, buffer);
stream.Flush(); stream.Flush();
@ -311,10 +316,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <param name="image"> /// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data. /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param> /// </param>
private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image) private void WriteImage<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.PixelBuffer; Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case BmpBitsPerPixel.Pixel32: case BmpBitsPerPixel.Pixel32:
@ -433,8 +438,8 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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>
private void Write8BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image) private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
bool isL8 = typeof(TPixel) == typeof(L8); bool isL8 = typeof(TPixel) == typeof(L8);
@ -456,13 +461,15 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration); 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; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); 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="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="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Create a color palette with 256 different gray values. // Create a color palette with 256 different gray values.
@ -503,7 +510,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
} }
stream.Write(colorPalette); stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.PixelBuffer; Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer();
for (int y = image.Height - 1; y >= 0; y--) for (int y = image.Height - 1; y >= 0; y--)
{ {
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(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> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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="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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{ {
MaxColors = 16 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -567,14 +577,17 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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="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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{ {
MaxColors = 4 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -620,14 +633,17 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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="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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{ {
MaxColors = 2 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); 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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
/// Image encoder for writing image data to a stream in gif format. /// Image encoder for writing image data to a stream in gif format.
/// </summary> /// </summary>
public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions public sealed class GifEncoder : QuantizingImageEncoder
{ {
/// <summary> /// <summary>
/// Gets or sets the quantizer for reducing the color count. /// Gets the color table mode: Global or local.
/// Defaults to the <see cref="OctreeQuantizer"/>
/// </summary> /// </summary>
public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree; public GifColorTableMode? ColorTableMode { get; init; }
/// <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();
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new GifEncoderCore(image.GetConfiguration(), this); GifEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new GifEncoderCore(image.GetConfiguration(), this); GifEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken); return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

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

@ -33,6 +33,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly byte[] buffer = new byte[20]; private readonly byte[] buffer = new byte[20];
/// <summary>
/// Whether to skip metadata during encode.
/// </summary>
private readonly bool skipMetadata;
/// <summary> /// <summary>
/// The quantizer used to generate the color palette. /// The quantizer used to generate the color palette.
/// </summary> /// </summary>
@ -57,14 +62,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The options for the encoder.</param> /// <param name="encoder">The encoder with options.</param>
public GifEncoderCore(Configuration configuration, IGifEncoderOptions options) public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{ {
this.configuration = configuration; this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
this.quantizer = options.Quantizer; this.skipMetadata = encoder.SkipMetadata;
this.colorTableMode = options.ColorTableMode; this.quantizer = encoder.Quantizer;
this.pixelSamplingStrategy = options.GlobalPixelSamplingStrategy; this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
} }
/// <summary> /// <summary>
@ -97,7 +103,8 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
else 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); this.WriteColorTable(quantized, stream);
} }
// Write the comments. if (!this.skipMetadata)
this.WriteComments(gifMetadata, stream); {
// Write the comments.
this.WriteComments(gifMetadata, stream);
// Write application extensions. // Write application extensions.
XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
if (useGlobalTable) 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, Stream stream,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using var bufferedReadStream = new BufferedReadStream(configuration, stream); using BufferedReadStream bufferedReadStream = new(configuration, stream);
try try
{ {
@ -86,7 +86,7 @@ internal static class ImageDecoderUtilities
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using var bufferedReadStream = new BufferedReadStream(configuration, stream); using BufferedReadStream bufferedReadStream = new(configuration, stream);
try 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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
@ -22,11 +22,11 @@ internal static class ImageEncoderUtilities
} }
else else
{ {
using var ms = new MemoryStream(); using MemoryStream ms = new();
await DoEncodeAsync(ms); await DoEncodeAsync(ms);
ms.Position = 0; ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
Task DoEncodeAsync(Stream innerStream) 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg; namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary> /// <summary>
/// Encoder for writing the data image to a stream in jpeg format. /// Encoder for writing the data image to a stream in jpeg format.
/// </summary> /// </summary>
public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions public sealed class JpegEncoder : ImageEncoder
{ {
/// <summary> /// <summary>
/// Backing field for <see cref="Quality"/>. /// Backing field for <see cref="Quality"/>.
/// </summary> /// </summary>
private int? quality; 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 public int? Quality
{ {
get => this.quality; get => this.quality;
set init
{ {
if (value is < 1 or > 100) if (value is < 1 or > 100)
{ {
@ -30,37 +33,31 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions
} }
} }
/// <inheritdoc/> /// <summary>
public bool? Interleaved { get; set; } /// Gets the component encoding mode.
/// </summary>
/// <inheritdoc/> /// <remarks>
public JpegEncodingColor? ColorType { get; set; } /// 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> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Gets the jpeg color for encoding.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> public JpegEncodingColor? ColorType { get; init; }
/// <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> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new JpegEncoderCore(this); JpegEncoderCore encoder = new(this);
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <summary> /// <inheritdoc/>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <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>
{ {
var encoder = new JpegEncoderCore(this); JpegEncoderCore encoder = new(this);
return encoder.EncodeAsync(image, stream, cancellationToken); 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> /// </summary>
private readonly byte[] buffer = new byte[20]; private readonly byte[] buffer = new byte[20];
private readonly IJpegEncoderOptions options; private readonly JpegEncoder encoder;
/// <summary> /// <summary>
/// The output stream. All attempted writes after the first error become no-ops. /// 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> /// <summary>
/// Initializes a new instance of the <see cref="JpegEncoderCore"/> class. /// Initializes a new instance of the <see cref="JpegEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="encoder">The parent encoder.</param>
public JpegEncoderCore(IJpegEncoderOptions options) public JpegEncoderCore(JpegEncoder encoder)
=> this.options = options; => this.encoder = encoder;
public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4]; public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4];
@ -71,8 +71,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata); JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata);
bool interleaved = this.options.Interleaved ?? jpegMetadata.Interleaved ?? true; bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved ?? true;
using var frame = new JpegFrame(image, frameConfig, interleaved); using JpegFrame frame = new(image, frameConfig, interleaved);
// Write the Start Of Image marker. // Write the Start Of Image marker.
this.WriteStartOfImage(); this.WriteStartOfImage();
@ -96,14 +96,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
this.WriteStartOfFrame(image.Width, image.Height, frameConfig); this.WriteStartOfFrame(image.Width, image.Height, frameConfig);
// Write the Huffman tables. // Write the Huffman tables.
var scanEncoder = new HuffmanScanEncoder(frame.BlocksPerMcu, stream); HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder);
// Write the quantization tables. // 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 // 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); this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken);
// Write the End Of Image marker. // Write the End Of Image marker.
@ -172,6 +172,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the Define Huffman Table marker and tables. /// Writes the Define Huffman Table marker and tables.
/// </summary> /// </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) private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder)
{ {
if (tableConfigs is null) if (tableConfigs is null)
@ -203,6 +206,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the APP14 marker to indicate the image is in RGB color space. /// Writes the APP14 marker to indicate the image is in RGB color space.
/// </summary> /// </summary>
/// <param name="colorTransform">The color transform byte.</param>
private void WriteApp14Marker(byte colorTransform) private void WriteApp14Marker(byte colorTransform)
{ {
this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length); this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length);
@ -498,6 +502,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the Start Of Frame (Baseline) marker. /// Writes the Start Of Frame (Baseline) marker.
/// </summary> /// </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) private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame)
{ {
JpegComponentConfig[] components = frame.Components; JpegComponentConfig[] components = frame.Components;
@ -536,6 +543,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the StartOfScan marker. /// Writes the StartOfScan marker.
/// </summary> /// </summary>
/// <param name="components">The collecction of component configuration items.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components) private void WriteStartOfScan(Span<JpegComponentConfig> components)
{ {
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // 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> /// <summary>
/// Writes scans for given config. /// Writes scans for given config.
/// </summary> /// </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> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (frame.Components.Length == 1) if (frame.Components.Length == 1)
@ -696,7 +715,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
private JpegFrameConfig GetFrameConfig(JpegMetadata metadata) 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( JpegFrameConfig frameConfig = Array.Find(
FrameConfigs, FrameConfigs,
cfg => cfg.EncodingColor == color); cfg => cfg.EncodingColor == color);

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

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm; namespace SixLabors.ImageSharp.Formats.Pbm;
@ -30,36 +29,34 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// </para> /// </para>
/// The specification of these images is found at <seealso href="http://netpbm.sourceforge.net/doc/pnm.html"/>. /// The specification of these images is found at <seealso href="http://netpbm.sourceforge.net/doc/pnm.html"/>.
/// </summary> /// </summary>
public sealed class PbmEncoder : IImageEncoder, IPbmEncoderOptions public sealed class PbmEncoder : ImageEncoder
{ {
/// <summary> /// <summary>
/// Gets or sets the Encoding of the pixels. /// Gets the encoding of the pixels.
/// </summary> /// </summary>
public PbmEncoding? Encoding { get; set; } public PbmEncoding? Encoding { get; init; }
/// <summary> /// <summary>
/// Gets or sets the Color type of the resulting image. /// Gets the Color type of the resulting image.
/// </summary> /// </summary>
public PbmColorType? ColorType { get; set; } public PbmColorType? ColorType { get; init; }
/// <summary> /// <summary>
/// Gets or sets the data type of the pixels components. /// Gets the Data Type of the pixel components.
/// </summary> /// </summary>
public PbmComponentType? ComponentType { get; set; } public PbmComponentType? ComponentType { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new PbmEncoderCore(image.GetConfiguration(), this); PbmEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new PbmEncoderCore(image.GetConfiguration(), this); PbmEncoderCore encoder = new(image.GetConfiguration(), this);
return encoder.EncodeAsync(image, stream, cancellationToken); 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; private Configuration configuration;
/// <summary> /// <summary>
/// The encoder options. /// The encoder with options.
/// </summary> /// </summary>
private readonly IPbmEncoderOptions options; private readonly PbmEncoder encoder;
/// <summary> /// <summary>
/// The encoding for the pixels. /// The encoding for the pixels.
@ -45,11 +45,11 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
/// Initializes a new instance of the <see cref="PbmEncoderCore"/> class. /// Initializes a new instance of the <see cref="PbmEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
/// <param name="options">The encoder options.</param> /// <param name="encoder">The encoder with options.</param>
public PbmEncoderCore(Configuration configuration, IPbmEncoderOptions options) public PbmEncoderCore(Configuration configuration, PbmEncoder encoder)
{ {
this.configuration = configuration; this.configuration = configuration;
this.options = options; this.encoder = encoder;
} }
/// <summary> /// <summary>
@ -65,7 +65,7 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.DeduceOptions(image); this.SanitizeAndSetEncoderOptions(image);
byte signature = this.DeduceSignature(); byte signature = this.DeduceSignature();
this.WriteHeader(stream, signature, image.Size()); this.WriteHeader(stream, signature, image.Size());
@ -75,16 +75,16 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
stream.Flush(); stream.Flush();
} }
private void DeduceOptions<TPixel>(Image<TPixel> image) private void SanitizeAndSetEncoderOptions<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.configuration = image.GetConfiguration(); this.configuration = image.GetConfiguration();
PbmMetadata metadata = image.Metadata.GetPbmMetadata(); PbmMetadata metadata = image.Metadata.GetPbmMetadata();
this.encoding = this.options.Encoding ?? metadata.Encoding; this.encoding = this.encoder.Encoding ?? metadata.Encoding;
this.colorType = this.options.ColorType ?? metadata.ColorType; this.colorType = this.encoder.ColorType ?? metadata.ColorType;
if (this.colorType != PbmColorType.BlackAndWhite) if (this.colorType != PbmColorType.BlackAndWhite)
{ {
this.componentType = this.options.ComponentType ?? metadata.ComponentType; this.componentType = this.encoder.ComponentType ?? metadata.ComponentType;
} }
else 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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Image encoder for writing image data to a stream in png format. /// Image encoder for writing image data to a stream in png format.
/// </summary> /// </summary>
public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public class PngEncoder : QuantizingImageEncoder
{ {
/// <inheritdoc/> /// <summary>
public PngBitDepth? BitDepth { get; set; } /// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
/// <inheritdoc/> public PngEncoder() =>
public PngColorType? ColorType { get; set; }
/// <inheritdoc/> // We set the quantizer to null here to allow the underlying encoder to create a
public PngFilterMethod? FilterMethod { get; set; } // quantizer with options appropriate to the encoding bit depth.
this.Quantizer = null;
/// <inheritdoc/> /// <summary>
public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression; /// 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/> /// <summary>
public int TextCompressionThreshold { get; set; } = 1024; /// Gets the color type.
/// </summary>
public PngColorType? ColorType { get; init; }
/// <inheritdoc/> /// <summary>
public float? Gamma { get; set; } /// Gets the filter method.
/// </summary>
public PngFilterMethod? FilterMethod { get; init; }
/// <inheritdoc/> /// <summary>
public IQuantizer Quantizer { get; set; } /// Gets the compression level 1-9.
/// <remarks>Defaults to <see cref="PngCompressionLevel.DefaultCompression" />.</remarks>
/// </summary>
public PngCompressionLevel CompressionLevel { get; init; } = PngCompressionLevel.DefaultCompression;
/// <inheritdoc/> /// <summary>
public byte Threshold { get; set; } = byte.MaxValue; /// Gets the threshold of characters in text metadata, when compression should be used.
/// </summary>
public int TextCompressionThreshold { get; init; } = 1024;
/// <inheritdoc/> /// <summary>
public PngInterlaceMode? InterlaceMethod { get; set; } /// 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/> /// <summary>
public PngChunkFilter? ChunkFilter { get; set; } /// Gets the transparency threshold.
/// </summary>
public byte Threshold { get; init; } = byte.MaxValue;
/// <inheritdoc/> /// <summary>
public bool IgnoreMetadata { get; set; } /// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; init; }
/// <inheritdoc/> /// <summary>
public PngTransparentColorMode TransparentColorMode { get; set; } /// Gets the chunk filter method. This allows to filter ancillary chunks.
/// </summary>
public PngChunkFilter? ChunkFilter { get; init; }
/// <summary> /// <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> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> public PngTransparentColorMode TransparentColorMode { get; init; }
/// <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> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
{ encoder.Encode(image, stream);
encoder.Encode(image, stream);
}
} }
/// <summary> /// <inheritdoc/>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. public override async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <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>
{ {
// The introduction of a local variable that refers to an object the implements // 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 // IDisposable means you must use async/await, where the compiler generates the
// state machine and a continuation. // state machine and a continuation.
using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this);
{ await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
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.Buffers.Binary;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -12,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -46,17 +48,42 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private readonly byte[] chunkDataBuffer = new byte[16]; private readonly byte[] chunkDataBuffer = new byte[16];
/// <summary> /// <summary>
/// The encoder options /// The encoder with options
/// </summary> /// </summary>
private readonly PngEncoderOptions options; private readonly PngEncoder encoder;
/// <summary> /// <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> /// </summary>
private byte bitDepth; private byte bitDepth;
/// <summary> /// <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> /// </summary>
private bool use16Bit; private bool use16Bit;
@ -95,12 +122,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param> /// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
/// <param name="options">The options for influencing the encoder</param> /// <param name="encoder">The encoder with options.</param>
public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoderOptions options) public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.options = options; this.encoder = encoder;
} }
/// <summary> /// <summary>
@ -122,16 +149,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); 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; Image<TPixel> clonedImage = null;
bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear; bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
if (clearTransparency) if (clearTransparency)
{ {
clonedImage = image.Clone(); clonedImage = image.Clone();
ClearTransparentPixels(clonedImage); ClearTransparentPixels(clonedImage);
} }
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage); IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
stream.Write(PngConstants.HeaderBytes); stream.Write(PngConstants.HeaderBytes);
@ -171,6 +198,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
where TPixel : unmanaged, IPixel<TPixel> => where TPixel : unmanaged, IPixel<TPixel> =>
image.ProcessPixelRows(accessor => image.ProcessPixelRows(accessor =>
{ {
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 rgba32 = default; Rgba32 rgba32 = default;
Rgba32 transparent = Color.Transparent; Rgba32 transparent = Color.Transparent;
for (int y = 0; y < accessor.Height; y++) for (int y = 0; y < accessor.Height; y++)
@ -189,27 +217,28 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}); });
/// <summary> /// <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> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param> /// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param> /// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <returns>The quantized image.</returns> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
IndexedImageFrame<TPixel> quantized; IndexedImageFrame<TPixel> quantized;
if (this.options.TransparentColorMode == PngTransparentColorMode.Clear) if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
{ {
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage); quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
} }
else else
{ {
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
} }
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized; return quantized;
} }
@ -223,23 +252,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> rawScanlineSpan = this.currentScanline.GetSpan(); Span<byte> rawScanlineSpan = this.currentScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan); ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.options.ColorType == PngColorType.Grayscale) if (this.colorType == PngColorType.Grayscale)
{ {
if (this.use16Bit) if (this.use16Bit)
{ {
// 16 bit grayscale // 16 bit grayscale
using (IMemoryOwner<L16> luminanceBuffer = this.memoryAllocator.Allocate<L16>(rowSpan.Length)) using IMemoryOwner<L16> luminanceBuffer = this.memoryAllocator.Allocate<L16>(rowSpan.Length);
{ Span<L16> luminanceSpan = luminanceBuffer.GetSpan();
Span<L16> luminanceSpan = luminanceBuffer.GetSpan(); ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan); PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
// Can't map directly to byte array as it's big-endian. // Can't map directly to byte array as it's big-endian.
for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
{ {
L16 luminance = Unsafe.Add(ref luminanceRef, x); L16 luminance = Unsafe.Add(ref luminanceRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
}
} }
} }
else if (this.bitDepth == 8) 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) private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.options.ColorType) switch (this.colorType)
{ {
case PngColorType.Palette: case PngColorType.Palette:
@ -413,7 +440,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="attempt">Used for attempting optimized filtering.</param> /// <param name="attempt">Used for attempting optimized filtering.</param>
private void FilterPixelBytes(ref Span<byte> filter, ref Span<byte> attempt) private void FilterPixelBytes(ref Span<byte> filter, ref Span<byte> attempt)
{ {
switch (this.options.FilterMethod) switch (this.filterMethod)
{ {
case PngFilterMethod.None: case PngFilterMethod.None:
NoneFilter.Encode(this.currentScanline.GetSpan(), filter); 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. // Palette images don't compress well with adaptive filtering.
// Nor do images comprising a single row. // 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); NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
return; return;
@ -543,10 +570,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
width: this.width, width: this.width,
height: this.height, height: this.height,
bitDepth: this.bitDepth, bitDepth: this.bitDepth,
colorType: this.options.ColorType.Value, colorType: this.colorType,
compressionMethod: 0, // None compressionMethod: 0, // None
filterMethod: 0, filterMethod: 0,
interlaceMethod: this.options.InterlaceMethod.Value); interlaceMethod: this.interlaceMode);
header.WriteTo(this.chunkDataBuffer); header.WriteTo(this.chunkDataBuffer);
@ -593,7 +620,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
byte alpha = rgba.A; byte alpha = rgba.A;
Unsafe.Add(ref colorTableRef, i) = rgba.Rgb; Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
if (alpha > this.options.Threshold) if (alpha > this.encoder.Threshold)
{ {
alpha = byte.MaxValue; alpha = byte.MaxValue;
} }
@ -619,7 +646,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param> /// <param name="meta">The image metadata.</param>
private void WritePhysicalChunk(Stream stream, ImageMetadata meta) 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; return;
} }
@ -636,7 +663,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param> /// <param name="meta">The image metadata.</param>
private void WriteExifChunk(Stream stream, ImageMetadata meta) 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; return;
} }
@ -658,7 +685,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void WriteXmpChunk(Stream stream, ImageMetadata meta) private void WriteXmpChunk(Stream stream, ImageMetadata meta)
{ {
const int iTxtHeaderSize = 5; const int iTxtHeaderSize = 5;
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{ {
return; return;
} }
@ -731,7 +758,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param> /// <param name="meta">The image metadata.</param>
private void WriteTextChunks(Stream stream, PngMetadata meta) 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; return;
} }
@ -754,7 +781,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
// Write iTXt chunk. // Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); 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)) ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value); : PngConstants.TranslatedEncoding.GetBytes(textData.Value);
@ -768,7 +795,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
keywordBytes.CopyTo(outputBytes); keywordBytes.CopyTo(outputBytes);
int bytesWritten = keywordBytes.Length; int bytesWritten = keywordBytes.Length;
outputBytes[bytesWritten++] = 0; outputBytes[bytesWritten++] = 0;
if (textData.Value.Length > this.options.TextCompressionThreshold) if (textData.Value.Length > this.encoder.TextCompressionThreshold)
{ {
// Indicate that the text is compressed. // Indicate that the text is compressed.
outputBytes[bytesWritten++] = 1; outputBytes[bytesWritten++] = 1;
@ -788,7 +815,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
textBytes.CopyTo(outputBytes[bytesWritten..]); textBytes.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes); 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. // Write zTXt chunk.
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value)); byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value));
@ -827,7 +854,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private byte[] GetZlibCompressedBytes(byte[] dataBytes) private byte[] GetZlibCompressedBytes(byte[] dataBytes)
{ {
using MemoryStream memoryStream = new(); 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); deflateStream.Write(dataBytes);
} }
@ -842,15 +869,15 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteGammaChunk(Stream stream) private void WriteGammaChunk(Stream stream)
{ {
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk) if ((this.chunkFilter & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
{ {
return; return;
} }
if (this.options.Gamma > 0) if (this.gamma > 0)
{ {
// 4-byte unsigned integer of gamma * 100,000. // 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); BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), gammaValue);
@ -924,9 +951,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
using (MemoryStream memoryStream = new()) 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) if (quantized != null)
{ {
@ -1192,4 +1219,196 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ref IMemoryOwner<byte> current = ref this.currentScanline; ref IMemoryOwner<byte> current = ref this.currentScanline;
RuntimeUtility.Swap(ref prev, ref current); 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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tga; namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a targa truevision image. /// Image encoder for writing an image to a stream as a targa truevision image.
/// </summary> /// </summary>
public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions public sealed class TgaEncoder : ImageEncoder
{ {
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel. /// Gets the number of bits per pixel.
/// </summary> /// </summary>
public TgaBitsPerPixel? BitsPerPixel { get; set; } public TgaBitsPerPixel? BitsPerPixel { get; init; }
/// <summary> /// <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> /// </summary>
public TgaCompression Compression { get; set; } = TgaCompression.RunLength; public TgaCompression Compression { get; init; } = TgaCompression.RunLength;
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); TgaEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken); return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

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

@ -45,13 +45,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TgaEncoderCore"/> class. /// Initializes a new instance of the <see cref="TgaEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The encoder options.</param> /// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param> /// <param name="memoryAllocator">The memory manager.</param>
public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator) public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel; this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = options.Compression; this.compression = encoder.Compression;
} }
/// <summary> /// <summary>
@ -105,7 +105,9 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
cMapLength: 0, cMapLength: 0,
cMapDepth: 0, cMapDepth: 0,
xOffset: 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, width: (short)image.Width,
height: (short)image.Height, height: (short)image.Height,
pixelDepth: (byte)this.bitsPerPixel.Value, 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.Advanced;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Encoder for writing the data image to a stream in TIFF format. /// Encoder for writing the data image to a stream in TIFF format.
/// </summary> /// </summary>
public class TiffEncoder : IImageEncoder, ITiffEncoderOptions public class TiffEncoder : QuantizingImageEncoder
{ {
/// <inheritdoc/> /// <summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; } /// Gets the number of bits per pixel.
/// </summary>
/// <inheritdoc/> public TiffBitsPerPixel? BitsPerPixel { get; init; }
public TiffCompression? Compression { get; set; }
/// <summary>
/// <inheritdoc/> /// Gets the compression type to use.
public DeflateCompressionLevel? CompressionLevel { get; set; } /// </summary>
public TiffCompression? Compression { get; init; }
/// <inheritdoc/>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } /// <summary>
/// Gets the compression level 1-9 for the deflate compression mode.
/// <inheritdoc/> /// <remarks>Defaults to <see cref="DeflateCompressionLevel.DefaultCompression" />.</remarks>
public TiffPredictor? HorizontalPredictor { get; set; } /// </summary>
public DeflateCompressionLevel? CompressionLevel { get; init; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; } /// <summary>
/// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor.
/// <inheritdoc/> /// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used.
public void Encode<TPixel>(Image<TPixel> image, Stream stream) /// </summary>
where TPixel : unmanaged, IPixel<TPixel> 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); encode.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new TiffEncoderCore(this, image.GetMemoryAllocator()); TiffEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken); 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;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -40,10 +39,15 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
private Configuration configuration; private Configuration configuration;
/// <summary> /// <summary>
/// The quantizer for creating color palette image. /// The quantizer for creating color palette images.
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private readonly IQuantizer quantizer;
/// <summary>
/// The pixel sampling strategy for quantization.
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary> /// <summary>
/// Sets the deflate compression level. /// Sets the deflate compression level.
/// </summary> /// </summary>
@ -69,6 +73,11 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; 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(); private readonly List<(long, uint)> frameMarkers = new();
/// <summary> /// <summary>
@ -76,15 +85,17 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <param name="options">The options for the encoder.</param> /// <param name="options">The options for the encoder.</param>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator) public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation; this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.quantizer = options.Quantizer;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel; this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor; this.HorizontalPredictor = options.HorizontalPredictor;
this.CompressionType = options.Compression; this.CompressionType = options.Compression;
this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
this.skipMetadata = options.SkipMetadata;
} }
/// <summary> /// <summary>
@ -215,6 +226,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
this.PhotometricInterpretation, this.PhotometricInterpretation,
frame, frame,
this.quantizer, this.quantizer,
this.pixelSamplingStrategy,
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
entriesCollector, entriesCollector,
@ -226,7 +238,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
if (image != null) if (image != null)
{ {
entriesCollector.ProcessMetadata(image); entriesCollector.ProcessMetadata(image, this.skipMetadata);
} }
entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessFrameInfo(frame, imageMetadata);
@ -331,7 +343,12 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return nextIfdMarker; 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. // BitsPerPixel should be the primary source of truth for the encoder options.
if (bitsPerPixel.HasValue) 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 List<IExifValue> Entries { get; } = new List<IExifValue>();
public void ProcessMetadata(Image image) public void ProcessMetadata(Image image, bool skipMetadata)
=> new MetadataProcessor(this).Process(image); => new MetadataProcessor(this).Process(image, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata); => new FrameInfoProcessor(this).Process(frame, imageMetadata);
@ -41,7 +41,7 @@ internal class TiffEncoderEntriesCollector
private abstract class BaseProcessor private abstract class BaseProcessor
{ {
public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; protected BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector;
protected TiffEncoderEntriesCollector Collector { get; } 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; ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile); this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile);
this.ProcessMetadata(rootFrameExifProfile);
if (!skipMetadata)
{
this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile());
}
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
{ {
@ -72,39 +76,35 @@ internal class TiffEncoderEntriesCollector
} }
private static bool IsPureMetadata(ExifTag tag) private static bool IsPureMetadata(ExifTag tag)
{ => (ExifTagValue)(ushort)tag switch
switch ((ExifTagValue)(ushort)tag)
{ {
case ExifTagValue.DocumentName: ExifTagValue.DocumentName or
case ExifTagValue.ImageDescription: ExifTagValue.ImageDescription or
case ExifTagValue.Make: ExifTagValue.Make or
case ExifTagValue.Model: ExifTagValue.Model or
case ExifTagValue.Software: ExifTagValue.Software or
case ExifTagValue.DateTime: ExifTagValue.DateTime or
case ExifTagValue.Artist: ExifTagValue.Artist or
case ExifTagValue.HostComputer: ExifTagValue.HostComputer or
case ExifTagValue.TargetPrinter: ExifTagValue.TargetPrinter or
case ExifTagValue.XMP: ExifTagValue.XMP or
case ExifTagValue.Rating: ExifTagValue.Rating or
case ExifTagValue.RatingPercent: ExifTagValue.RatingPercent or
case ExifTagValue.ImageID: ExifTagValue.ImageID or
case ExifTagValue.Copyright: ExifTagValue.Copyright or
case ExifTagValue.MDLabName: ExifTagValue.MDLabName or
case ExifTagValue.MDSampleInfo: ExifTagValue.MDSampleInfo or
case ExifTagValue.MDPrepDate: ExifTagValue.MDPrepDate or
case ExifTagValue.MDPrepTime: ExifTagValue.MDPrepTime or
case ExifTagValue.MDFileUnits: ExifTagValue.MDFileUnits or
case ExifTagValue.SEMInfo: ExifTagValue.SEMInfo or
case ExifTagValue.XPTitle: ExifTagValue.XPTitle or
case ExifTagValue.XPComment: ExifTagValue.XPComment or
case ExifTagValue.XPAuthor: ExifTagValue.XPAuthor or
case ExifTagValue.XPKeywords: ExifTagValue.XPKeywords or
case ExifTagValue.XPSubject: ExifTagValue.XPSubject => true,
return true; _ => false,
default: };
return false;
}
}
private void ProcessMetadata(ExifProfile exifProfile) 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) foreach (IExifValue entry in exifProfile.Values)
{ {
@ -167,13 +167,13 @@ internal class TiffEncoderEntriesCollector
} }
else else
{ {
exifProfile.RemoveValue(ExifTag.SubIFDOffset); exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
} }
if (imageMetadata.IptcProfile != null) if (!skipMetadata && imageMetadata.IptcProfile != null)
{ {
imageMetadata.IptcProfile.UpdateData(); imageMetadata.IptcProfile.UpdateData();
var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte) ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
{ {
Value = imageMetadata.IptcProfile.Data Value = imageMetadata.IptcProfile.Data
}; };
@ -182,12 +182,12 @@ internal class TiffEncoderEntriesCollector
} }
else else
{ {
exifProfile.RemoveValue(ExifTag.IPTC); exifProfile?.RemoveValue(ExifTag.IPTC);
} }
if (imageMetadata.IccProfile != null) if (imageMetadata.IccProfile != null)
{ {
var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined) ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
{ {
Value = imageMetadata.IccProfile.ToByteArray() Value = imageMetadata.IccProfile.ToByteArray()
}; };
@ -196,12 +196,12 @@ internal class TiffEncoderEntriesCollector
} }
else 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 Value = xmpProfile.Data
}; };
@ -210,7 +210,7 @@ internal class TiffEncoderEntriesCollector
} }
else else
{ {
exifProfile.RemoveValue(ExifTag.XMP); exifProfile?.RemoveValue(ExifTag.XMP);
} }
} }
} }
@ -273,29 +273,29 @@ internal class TiffEncoderEntriesCollector
public void Process(TiffEncoderCore encoder) public void Process(TiffEncoderCore encoder)
{ {
var planarConfig = new ExifShort(ExifTagValue.PlanarConfiguration) ExifShort planarConfig = new(ExifTagValue.PlanarConfiguration)
{ {
Value = (ushort)TiffPlanarConfiguration.Chunky Value = (ushort)TiffPlanarConfiguration.Chunky
}; };
var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) ExifLong samplesPerPixel = new(ExifTagValue.SamplesPerPixel)
{ {
Value = GetSamplesPerPixel(encoder) Value = GetSamplesPerPixel(encoder)
}; };
ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder); ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder);
var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) ExifShortArray bitPerSample = new(ExifTagValue.BitsPerSample)
{ {
Value = bitsPerSampleValue Value = bitsPerSampleValue
}; };
ushort compressionType = GetCompressionType(encoder); ushort compressionType = GetCompressionType(encoder);
var compression = new ExifShort(ExifTagValue.Compression) ExifShort compression = new(ExifTagValue.Compression)
{ {
Value = compressionType Value = compressionType
}; };
var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) ExifShort photometricInterpretation = new(ExifTagValue.PhotometricInterpretation)
{ {
Value = (ushort)encoder.PhotometricInterpretation Value = (ushort)encoder.PhotometricInterpretation
}; };
@ -306,32 +306,25 @@ internal class TiffEncoderEntriesCollector
this.Collector.AddOrReplace(compression); this.Collector.AddOrReplace(compression);
this.Collector.AddOrReplace(photometricInterpretation); 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 || ExifShort predictor = new(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal };
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero)
{
var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal };
this.Collector.AddOrReplace(predictor); this.Collector.AddOrReplace(predictor);
}
} }
} }
private static uint GetSamplesPerPixel(TiffEncoderCore encoder) private static uint GetSamplesPerPixel(TiffEncoderCore encoder)
{ => encoder.PhotometricInterpretation switch
switch (encoder.PhotometricInterpretation)
{ {
case TiffPhotometricInterpretation.PaletteColor: TiffPhotometricInterpretation.PaletteColor or
case TiffPhotometricInterpretation.BlackIsZero: TiffPhotometricInterpretation.BlackIsZero or
case TiffPhotometricInterpretation.WhiteIsZero: TiffPhotometricInterpretation.WhiteIsZero => 1,
return 1; _ => 3,
case TiffPhotometricInterpretation.Rgb: };
default:
return 3;
}
}
private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder)
{ {
@ -342,10 +335,8 @@ internal class TiffEncoderEntriesCollector
{ {
return TiffConstants.BitsPerSample4Bit.ToArray(); return TiffConstants.BitsPerSample4Bit.ToArray();
} }
else
{ return TiffConstants.BitsPerSample8Bit.ToArray();
return TiffConstants.BitsPerSample8Bit.ToArray();
}
case TiffPhotometricInterpretation.Rgb: case TiffPhotometricInterpretation.Rgb:
return TiffConstants.BitsPerSampleRgb8Bit.ToArray(); return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
@ -382,9 +373,9 @@ internal class TiffEncoderEntriesCollector
// PackBits is allowed for all modes. // PackBits is allowed for all modes.
return (ushort)TiffCompression.PackBits; return (ushort)TiffCompression.PackBits;
case TiffCompression.Lzw: case TiffCompression.Lzw:
if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb || if (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor || TiffPhotometricInterpretation.PaletteColor or
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) TiffPhotometricInterpretation.BlackIsZero)
{ {
return (ushort)TiffCompression.Lzw; return (ushort)TiffCompression.Lzw;
} }

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

@ -14,6 +14,7 @@ internal static class TiffColorWriterFactory
TiffPhotometricInterpretation? photometricInterpretation, TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame<TPixel> image, ImageFrame<TPixel> image,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
@ -23,7 +24,7 @@ internal static class TiffColorWriterFactory
switch (photometricInterpretation) switch (photometricInterpretation)
{ {
case TiffPhotometricInterpretation.PaletteColor: 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.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero: case TiffPhotometricInterpretation.WhiteIsZero:
if (bitsPerPixel == 1) 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 maxColors;
private readonly int colorPaletteSize; private readonly int colorPaletteSize;
private readonly int colorPaletteBytes; private readonly int colorPaletteBytes;
private readonly IndexedImageFrame<TPixel> quantizedImage; private readonly IndexedImageFrame<TPixel> quantizedFrame;
private IMemoryOwner<byte> indexedPixelsBuffer; private IMemoryOwner<byte> indexedPixelsBuffer;
public TiffPaletteWriter( public TiffPaletteWriter(
ImageFrame<TPixel> image, ImageFrame<TPixel> frame,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel) int bitsPerPixel)
: base(image, memoryAllocator, configuration, entriesCollector) : base(frame, memoryAllocator, configuration, entriesCollector)
{ {
DebugGuard.NotNull(quantizer, nameof(quantizer)); DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
DebugGuard.NotNull(configuration, nameof(configuration)); DebugGuard.NotNull(configuration, nameof(configuration));
DebugGuard.NotNull(entriesCollector, nameof(entriesCollector)); DebugGuard.NotNull(entriesCollector, nameof(entriesCollector));
DebugGuard.MustBeBetweenOrEqualTo(bitsPerPixel, 4, 8, nameof(bitsPerPixel)); 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.maxColors = this.BitsPerPixel == 4 ? 16 : 256;
this.colorPaletteSize = this.maxColors * 3; this.colorPaletteSize = this.maxColors * 3;
this.colorPaletteBytes = this.colorPaletteSize * 2; this.colorPaletteBytes = this.colorPaletteSize * 2;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(
{ this.Configuration,
MaxColors = this.maxColors new QuantizerOptions()
}); {
this.quantizedImage = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); MaxColors = this.maxColors
});
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.AddColorMapTag(); this.AddColorMapTag();
} }
@ -66,7 +72,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
int lastRow = y + height; int lastRow = y + height;
for (int row = y; row < lastRow; row++) for (int row = y; row < lastRow; row++)
{ {
ReadOnlySpan<byte> indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row); ReadOnlySpan<byte> indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row);
int idxPixels = 0; int idxPixels = 0;
for (int x = 0; x < halfWidth; x++) for (int x = 0; x < halfWidth; x++)
{ {
@ -93,7 +99,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
int indexedPixelsRowIdx = 0; int indexedPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++) 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)); indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width));
indexedPixelsRowIdx++; indexedPixelsRowIdx++;
} }
@ -105,7 +111,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
this.quantizedImage?.Dispose(); this.quantizedFrame?.Dispose();
this.indexedPixelsBuffer?.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); using IMemoryOwner<byte> colorPaletteBuffer = this.MemoryAllocator.Allocate<byte>(this.colorPaletteBytes);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); 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; int quantizedColorBytes = quantizedColors.Length * 3 * 2;
// In the ColorMap, black is represented by 0, 0, 0 and white is represented by 65535, 65535, 65535. // 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, // 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. // 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; int paletteIdx = 0;
for (int i = 0; i < quantizedColors.Length; i++) for (int i = 0; i < quantizedColors.Length; i++)
{ {
@ -147,7 +153,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
palette[paletteIdx++] = quantizedColorRgb48[i].B; palette[paletteIdx++] = quantizedColorRgb48[i].B;
} }
var colorMap = new ExifShortArray(ExifTagValue.ColorMap) ExifShortArray colorMap = new(ExifTagValue.ColorMap)
{ {
Value = palette 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="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</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="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> /// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = image.Width;
@ -36,14 +37,15 @@ internal class AlphaEncoder : IDisposable
if (compress) if (compress)
{ {
WebpEncodingMethod effort = WebpEncodingMethod.Default; const WebpEncodingMethod effort = WebpEncodingMethod.Default;
int quality = 8 * (int)effort; const int quality = 8 * (int)effort;
using var lossLessEncoder = new Vp8LEncoder( using Vp8LEncoder lossLessEncoder = new(
memoryAllocator, memoryAllocator,
configuration, configuration,
width, width,
height, height,
quality, quality,
skipMetadata,
effort, effort,
WebpTransparentColorMode.Preserve, WebpTransparentColorMode.Preserve,
false, false,
@ -75,7 +77,7 @@ internal class AlphaEncoder : IDisposable
{ {
int width = image.Width; int width = image.Width;
int height = image.Height; int height = image.Height;
var alphaAsImage = new Image<Rgba32>(width, height); Image<Rgba32> alphaAsImage = new(width, height);
for (int y = 0; y < height; y++) 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.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless; namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@ -67,6 +69,11 @@ internal class Vp8LEncoder : IDisposable
/// </summary> /// </summary>
private readonly WebpTransparentColorMode transparentColorMode; private readonly WebpTransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
/// <summary> /// <summary>
/// Indicating whether near lossless mode should be used. /// Indicating whether near lossless mode should be used.
/// </summary> /// </summary>
@ -91,6 +98,7 @@ internal class Vp8LEncoder : IDisposable
/// <param name="width">The width of the input image.</param> /// <param name="width">The width of the input image.</param>
/// <param name="height">The height of the input image.</param> /// <param name="height">The height of the input image.</param>
/// <param name="quality">The encoding quality.</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="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. /// <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> /// Otherwise, discard this invisible RGB information for better compression.</param>
@ -102,6 +110,7 @@ internal class Vp8LEncoder : IDisposable
int width, int width,
int height, int height,
int quality, int quality,
bool skipMetadata,
WebpEncodingMethod method, WebpEncodingMethod method,
WebpTransparentColorMode transparentColorMode, WebpTransparentColorMode transparentColorMode,
bool nearLossless, bool nearLossless,
@ -113,6 +122,7 @@ internal class Vp8LEncoder : IDisposable
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.quality = Numerics.Clamp(quality, 0, 100); this.quality = Numerics.Clamp(quality, 0, 100);
this.skipMetadata = skipMetadata;
this.method = method; this.method = method;
this.transparentColorMode = transparentColorMode; this.transparentColorMode = transparentColorMode;
this.nearLossless = nearLossless; this.nearLossless = nearLossless;
@ -239,6 +249,9 @@ internal class Vp8LEncoder : IDisposable
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles(); metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
// Convert image pixels to bgra array. // Convert image pixels to bgra array.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
@ -252,7 +265,7 @@ internal class Vp8LEncoder : IDisposable
this.EncodeStream(image); this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream. // 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> /// <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.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossy; namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
@ -55,6 +57,11 @@ internal class Vp8Encoder : IDisposable
/// </summary> /// </summary>
private Vp8BitWriter bitWriter; private Vp8BitWriter bitWriter;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
private readonly Vp8RdLevel rdOptLevel; private readonly Vp8RdLevel rdOptLevel;
private int maxI4HeaderBits; private int maxI4HeaderBits;
@ -94,6 +101,7 @@ internal class Vp8Encoder : IDisposable
/// <param name="width">The width of the input image.</param> /// <param name="width">The width of the input image.</param>
/// <param name="height">The height of the input image.</param> /// <param name="height">The height of the input image.</param>
/// <param name="quality">The encoding quality.</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="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="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> /// <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 width,
int height, int height,
int quality, int quality,
bool skipMetadata,
WebpEncodingMethod method, WebpEncodingMethod method,
int entropyPasses, int entropyPasses,
int filterStrength, int filterStrength,
@ -116,6 +125,7 @@ internal class Vp8Encoder : IDisposable
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
this.quality = Numerics.Clamp(quality, 0, 100); this.quality = Numerics.Clamp(quality, 0, 100);
this.skipMetadata = skipMetadata;
this.method = method; this.method = method;
this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10);
this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100);
@ -342,7 +352,7 @@ internal class Vp8Encoder : IDisposable
if (hasAlpha) if (hasAlpha)
{ {
// TODO: This can potentially run in an separate task. // 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(); alphaData = encodedAlphaData.GetSpan();
if (alphaDataSize < pixelCount) if (alphaDataSize < pixelCount)
{ {
@ -384,10 +394,14 @@ internal class Vp8Encoder : IDisposable
// Write bytes from the bitwriter buffer to the stream. // Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles(); metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
this.bitWriter.WriteEncodedImageToStream( this.bitWriter.WriteEncodedImageToStream(
stream, stream,
metadata.ExifProfile, exifProfile,
metadata.XmpProfile, xmpProfile,
metadata.IccProfile, metadata.IccProfile,
(uint)width, (uint)width,
(uint)height, (uint)height,

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

@ -2,58 +2,94 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream in the Webp format. /// Image encoder for writing an image to a stream in the Webp format.
/// </summary> /// </summary>
public sealed class WebpEncoder : IImageEncoder, IWebpEncoderOptions public sealed class WebpEncoder : ImageEncoder
{ {
/// <inheritdoc/> /// <summary>
public WebpFileFormatType? FileFormat { get; set; } /// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.
/// </summary>
public WebpFileFormatType? FileFormat { get; init; }
/// <inheritdoc/> /// <summary>
public int Quality { get; set; } = 75; /// 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/> /// <summary>
public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default; /// 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/> /// <summary>
public bool UseAlphaCompression { get; set; } = true; /// 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/> /// <summary>
public int EntropyPasses { get; set; } = 1; /// Gets the number of entropy-analysis passes (in [1..10]).
/// Defaults to 1.
/// </summary>
public int EntropyPasses { get; init; } = 1;
/// <inheritdoc/> /// <summary>
public int SpatialNoiseShaping { get; set; } = 50; /// 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/> /// <summary>
public int FilterStrength { get; set; } = 60; /// 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/> /// <summary>
public WebpTransparentColorMode TransparentColorMode { get; set; } = WebpTransparentColorMode.Clear; /// 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/> /// <summary>
public bool NearLossless { get; set; } /// 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/> /// <summary>
public int NearLosslessQuality { get; set; } = 100; /// 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/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); WebpEncoderCore encoder = new(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken); return encoder.EncodeAsync(image, stream, cancellationToken);
} }
} }

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

@ -56,6 +56,11 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly WebpTransparentColorMode transparentColorMode; private readonly WebpTransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
/// <summary> /// <summary>
/// Indicating whether near lossless mode should be used. /// Indicating whether near lossless mode should be used.
/// </summary> /// </summary>
@ -80,21 +85,22 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpEncoderCore"/> class. /// Initializes a new instance of the <see cref="WebpEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The encoder options.</param> /// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The memory manager.</param> /// <param name="memoryAllocator">The memory manager.</param>
public WebpEncoderCore(IWebpEncoderOptions options, MemoryAllocator memoryAllocator) public WebpEncoderCore(WebpEncoder encoder, MemoryAllocator memoryAllocator)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.alphaCompression = options.UseAlphaCompression; this.alphaCompression = encoder.UseAlphaCompression;
this.fileFormat = options.FileFormat; this.fileFormat = encoder.FileFormat;
this.quality = options.Quality; this.quality = encoder.Quality;
this.method = options.Method; this.method = encoder.Method;
this.entropyPasses = options.EntropyPasses; this.entropyPasses = encoder.EntropyPasses;
this.spatialNoiseShaping = options.SpatialNoiseShaping; this.spatialNoiseShaping = encoder.SpatialNoiseShaping;
this.filterStrength = options.FilterStrength; this.filterStrength = encoder.FilterStrength;
this.transparentColorMode = options.TransparentColorMode; this.transparentColorMode = encoder.TransparentColorMode;
this.nearLossless = options.NearLossless; this.skipMetadata = encoder.SkipMetadata;
this.nearLosslessQuality = options.NearLosslessQuality; this.nearLossless = encoder.NearLossless;
this.nearLosslessQuality = encoder.NearLosslessQuality;
} }
/// <summary> /// <summary>
@ -124,12 +130,13 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless) if (lossless)
{ {
using var enc = new Vp8LEncoder( using Vp8LEncoder enc = new(
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
image.Height, image.Height,
this.quality, this.quality,
this.skipMetadata,
this.method, this.method,
this.transparentColorMode, this.transparentColorMode,
this.nearLossless, this.nearLossless,
@ -138,12 +145,13 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
} }
else else
{ {
using var enc = new Vp8Encoder( using Vp8Encoder enc = new(
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
image.Height, image.Height,
this.quality, this.quality,
this.skipMetadata,
this.method, this.method,
this.entropyPasses, this.entropyPasses,
this.filterStrength, 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. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp;
public static partial class ImageExtensions public static partial class ImageExtensions
{ {
/// <summary> /// <summary>
/// Locks the image providing access to the pixels. /// Provides access to the image pixels.
/// <remarks> /// <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> /// </remarks>
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
@ -24,7 +24,5 @@ public static partial class ImageExtensions
/// </returns> /// </returns>
internal static Buffer2D<TPixel> GetRootFramePixelBuffer<TPixel>(this Image<TPixel> image) internal static Buffer2D<TPixel> GetRootFramePixelBuffer<TPixel>(this Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => image.Frames.RootFrame.PixelBuffer;
return image.Frames.RootFrame.PixelBuffer;
}
} }

6
src/ImageSharp/ImageFrameCollection.cs

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

17
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -168,7 +168,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
var frame = ImageFrame.LoadPixelData( ImageFrame<TPixel> frame = ImageFrame.LoadPixelData(
this.parent.GetConfiguration(), this.parent.GetConfiguration(),
source, source,
this.RootFrame.Width, this.RootFrame.Width,
@ -298,7 +298,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
var frame = new ImageFrame<TPixel>( ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(), this.parent.GetConfiguration(),
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height); this.RootFrame.Height);
@ -364,7 +364,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
/// </returns> /// </returns>
public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor) public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor)
{ {
var frame = new ImageFrame<TPixel>( ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(), this.parent.GetConfiguration(),
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height, this.RootFrame.Height,
@ -374,10 +374,15 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator<ImageFrame<TPixel>> IEnumerable<ImageFrame<TPixel>>.GetEnumerator() => this.frames.GetEnumerator(); public IEnumerator<ImageFrame<TPixel>> GetEnumerator()
{
this.EnsureNotDisposed();
return this.frames.GetEnumerator();
}
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.frames).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private void ValidateFrame(ImageFrame<TPixel> frame) private void ValidateFrame(ImageFrame<TPixel> frame)
{ {
@ -408,7 +413,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source) private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source)
{ {
var result = new ImageFrame<TPixel>( ImageFrame<TPixel> result = new(
this.parent.GetConfiguration(), this.parent.GetConfiguration(),
source.Size(), source.Size(),
source.Metadata.DeepClone()); 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> /// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, ImageMetadata metadata) internal Image(Configuration configuration, int width, int height, ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : 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> /// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class /// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -115,9 +113,7 @@ public sealed class Image<TPixel> : Image
int height, int height,
ImageMetadata metadata) ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : 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> /// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class /// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -135,9 +131,7 @@ public sealed class Image<TPixel> : Image
TPixel backgroundColor, TPixel backgroundColor,
ImageMetadata metadata) ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : 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> /// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}" /> class /// 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> /// <param name="frames">The frames that will be owned by this image instance.</param>
internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<ImageFrame<TPixel>> frames) internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<ImageFrame<TPixel>> frames)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, ValidateFramesAndGetSize(frames)) : base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, ValidateFramesAndGetSize(frames))
{ => this.frames = new ImageFrameCollection<TPixel>(this, frames);
this.frames = new ImageFrameCollection<TPixel>(this, frames);
}
/// <inheritdoc /> /// <inheritdoc />
protected override ImageFrameCollection NonGenericFrameCollection => this.Frames; 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> /// <exception cref="ArgumentOutOfRangeException">Thrown when the provided (x,y) coordinates are outside the image boundary.</exception>
public TPixel this[int x, int y] public TPixel this[int x, int y]
{ {
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
get get
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
@ -190,7 +182,7 @@ public sealed class Image<TPixel> : Image
return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y); return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y);
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
set set
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
@ -212,7 +204,7 @@ public sealed class Image<TPixel> : Image
try try
{ {
var accessor = new PixelAccessor<TPixel>(buffer); PixelAccessor<TPixel> accessor = new(buffer);
processPixels(accessor); processPixels(accessor);
} }
finally finally
@ -243,8 +235,8 @@ public sealed class Image<TPixel> : Image
try try
{ {
var accessor1 = new PixelAccessor<TPixel>(buffer1); PixelAccessor<TPixel> accessor1 = new(buffer1);
var accessor2 = new PixelAccessor<TPixel2>(buffer2); PixelAccessor<TPixel2> accessor2 = new(buffer2);
processPixels(accessor1, accessor2); processPixels(accessor1, accessor2);
} }
finally finally
@ -283,9 +275,9 @@ public sealed class Image<TPixel> : Image
try try
{ {
var accessor1 = new PixelAccessor<TPixel>(buffer1); PixelAccessor<TPixel> accessor1 = new(buffer1);
var accessor2 = new PixelAccessor<TPixel2>(buffer2); PixelAccessor<TPixel2> accessor2 = new(buffer2);
var accessor3 = new PixelAccessor<TPixel3>(buffer3); PixelAccessor<TPixel3> accessor3 = new(buffer3);
processPixels(accessor1, accessor2, accessor3); processPixels(accessor1, accessor2, accessor3);
} }
finally finally
@ -348,7 +340,7 @@ public sealed class Image<TPixel> : Image
{ {
this.EnsureNotDisposed(); 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++) for (int i = 0; i < clonedFrames.Length; i++)
{ {
clonedFrames[i] = this.frames[i].Clone(configuration); clonedFrames[i] = this.frames[i].Clone(configuration);
@ -367,7 +359,7 @@ public sealed class Image<TPixel> : Image
{ {
this.EnsureNotDisposed(); 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++) for (int i = 0; i < clonedFrames.Length; i++)
{ {
clonedFrames[i] = this.frames[i].CloneAs<TPixel2>(configuration); clonedFrames[i] = this.frames[i].CloneAs<TPixel2>(configuration);
@ -444,7 +436,7 @@ public sealed class Image<TPixel> : Image
return rootSize; return rootSize;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void VerifyCoords(int x, int y) private void VerifyCoords(int x, int y)
{ {
if ((uint)x >= (uint)this.Width) 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) 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. 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 denom = (int)ratio.Denominator;
int num = (int)ratio.Numerator; int num = (int)ratio.Numerator;
DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero.");
for (int pos = 0; pos < totalNumberOfRows; pos++) for (int pos = 0; pos < totalNumberOfRows; pos++)
{ {
int subPos = pos % denom; int subPos = (int)((uint)pos % (uint)denom);
if (subPos < num) if (subPos < num)
{ {
yield return GetRow(pos); 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(); 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; namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// <summary> /// <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> /// </summary>
public interface IPixelSamplingStrategy public interface IPixelSamplingStrategy
{ {
/// <summary> /// <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> /// </summary>
/// <param name="image">The image.</param> /// <param name="image">The image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>An enumeration of pixel regions.</returns> /// <returns>An enumeration of pixel regions.</returns>
IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(Image<TPixel> image) IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>; 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(quantizer, nameof(quantizer));
Guard.NotNull(source, nameof(source)); 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); Buffer2DRegion<TPixel> region = source.PixelBuffer.GetRegion(interest);
// Collect the palette. Required before the second pass runs. // Collect the palette. Required before the second pass runs.
@ -77,9 +77,9 @@ public static class QuantizerUtilities
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Guard.NotNull(source, nameof(source)); 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, quantizer.Configuration,
interest.Width, interest.Width,
interest.Height, interest.Height,
@ -99,13 +99,39 @@ public static class QuantizerUtilities
return destination; 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, this IQuantizer<TPixel> quantizer,
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> image) ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(image)) foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{ {
quantizer.AddPaletteColors(region); quantizer.AddPaletteColors(region);
} }

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

@ -11,42 +11,40 @@ namespace SixLabors.ImageSharp.Tests.Drawing;
[GroupOutput("Drawing")] [GroupOutput("Drawing")]
public class DrawImageTests public class DrawImageTests
{ {
public static readonly TheoryData<PixelColorBlendingMode> BlendingModes = new TheoryData<PixelColorBlendingMode> public static readonly TheoryData<PixelColorBlendingMode> BlendingModes = new()
{ {
PixelColorBlendingMode.Normal, PixelColorBlendingMode.Normal,
PixelColorBlendingMode.Multiply, PixelColorBlendingMode.Multiply,
PixelColorBlendingMode.Add, PixelColorBlendingMode.Add,
PixelColorBlendingMode.Subtract, PixelColorBlendingMode.Subtract,
PixelColorBlendingMode.Screen, PixelColorBlendingMode.Screen,
PixelColorBlendingMode.Darken, PixelColorBlendingMode.Darken,
PixelColorBlendingMode.Lighten, PixelColorBlendingMode.Lighten,
PixelColorBlendingMode.Overlay, PixelColorBlendingMode.Overlay,
PixelColorBlendingMode.HardLight, PixelColorBlendingMode.HardLight,
}; };
[Theory] [Theory]
[WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)] [WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)]
public void ImageBlendingMatchesSvgSpecExamples<TPixel>(TestImageProvider<TPixel> provider, PixelColorBlendingMode mode) public void ImageBlendingMatchesSvgSpecExamples<TPixel>(TestImageProvider<TPixel> provider, PixelColorBlendingMode mode)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> background = provider.GetImage()) using Image<TPixel> background = provider.GetImage();
using (var source = Image.Load<TPixel>(TestFile.Create(TestImages.Png.Ducky).Bytes)) using Image<TPixel> source = Image.Load<TPixel>(TestFile.Create(TestImages.Png.Ducky).Bytes);
{ background.Mutate(x => x.DrawImage(source, mode, 1F));
background.Mutate(x => x.DrawImage(source, mode, 1F)); background.DebugSave(
background.DebugSave( provider,
provider, new { mode },
new { mode = mode }, appendPixelTypeToFileName: false,
appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
appendSourceFileOrDescription: false);
ImageComparer comparer = ImageComparer.TolerantPercentage(0.01F);
var comparer = ImageComparer.TolerantPercentage(0.01F); background.CompareToReferenceOutput(
background.CompareToReferenceOutput( comparer,
comparer, provider,
provider, new { mode },
new { mode = mode }, appendPixelTypeToFileName: false,
appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
appendSourceFileOrDescription: false);
}
} }
[Theory] [Theory]
@ -68,28 +66,29 @@ public class DrawImageTests
float opacity) float opacity)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using Image<TPixel> image = provider.GetImage();
using (var blend = Image.Load<TPixel>(TestFile.Create(brushImage).Bytes)) 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); encoder = new() { BitDepth = PngBitDepth.Bit16 };
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);
} }
else
{
encoder = new();
}
image.DebugSave(provider, testInfo, encoder: encoder);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.01f),
provider,
testInfo);
} }
[Theory] [Theory]
@ -99,19 +98,17 @@ public class DrawImageTests
{ {
byte[] brushData = TestFile.Create(TestImages.Png.Ducky).Bytes; byte[] brushData = TestFile.Create(TestImages.Png.Ducky).Bytes;
using (Image<TPixel> image = provider.GetImage()) using Image<TPixel> image = provider.GetImage();
using (Image brushImage = provider.PixelType == PixelTypes.Rgba32 using Image brushImage = provider.PixelType == PixelTypes.Rgba32
? (Image)Image.Load<Bgra32>(brushData) ? Image.Load<Bgra32>(brushData)
: Image.Load<Rgba32>(brushData)) : Image.Load<Rgba32>(brushData);
{ image.Mutate(c => c.DrawImage(brushImage, 0.5f));
image.Mutate(c => c.DrawImage(brushImage, 0.5f));
image.DebugSave(provider, appendSourceFileOrDescription: false);
image.DebugSave(provider, appendSourceFileOrDescription: false); image.CompareToReferenceOutput(
image.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.01f),
ImageComparer.TolerantPercentage(0.01f), provider,
provider, appendSourceFileOrDescription: false);
appendSourceFileOrDescription: false);
}
} }
[Theory] [Theory]
@ -121,26 +118,24 @@ public class DrawImageTests
[WithSolidFilledImages(100, 100, "White", PixelTypes.Rgba32, -25, -30)] [WithSolidFilledImages(100, 100, "White", PixelTypes.Rgba32, -25, -30)]
public void WorksWithDifferentLocations(TestImageProvider<Rgba32> provider, int x, int y) public void WorksWithDifferentLocations(TestImageProvider<Rgba32> provider, int x, int y)
{ {
using (Image<Rgba32> background = provider.GetImage()) using Image<Rgba32> background = provider.GetImage();
using (var overlay = new Image<Rgba32>(50, 50)) using Image<Rgba32> overlay = new(50, 50);
{ Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> overlayMem));
Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> overlayMem)); overlayMem.Span.Fill(Color.Black);
overlayMem.Span.Fill(Color.Black);
background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F));
background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F));
background.DebugSave(
background.DebugSave( provider,
provider, testOutputDetails: $"{x}_{y}",
testOutputDetails: $"{x}_{y}", appendPixelTypeToFileName: false,
appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
background.CompareToReferenceOutput( provider,
provider, testOutputDetails: $"{x}_{y}",
testOutputDetails: $"{x}_{y}", appendPixelTypeToFileName: false,
appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
appendSourceFileOrDescription: false);
}
} }
[Theory] [Theory]
@ -148,29 +143,27 @@ public class DrawImageTests
public void DrawTransformed<TPixel>(TestImageProvider<TPixel> provider) public void DrawTransformed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using Image<TPixel> image = provider.GetImage();
using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes)) using Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes);
{ AffineTransformBuilder builder = new AffineTransformBuilder()
AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(45F)
.AppendRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F))
.AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10));
.AppendTranslation(new PointF(10, 10));
// Apply a background color so we can see the translation.
// Apply a background color so we can see the translation. blend.Mutate(x => x.Transform(builder));
blend.Mutate(x => x.Transform(builder)); blend.Mutate(x => x.BackgroundColor(Color.HotPink));
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
// 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);
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); image.Mutate(x => x.DrawImage(blend, position, .75F));
image.Mutate(x => x.DrawImage(blend, position, .75F));
image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false);
image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); image.CompareToReferenceOutput(
image.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.002f),
ImageComparer.TolerantPercentage(0.002f), provider,
provider, appendSourceFileOrDescription: false,
appendSourceFileOrDescription: false, appendPixelTypeToFileName: false);
appendPixelTypeToFileName: false);
}
} }
[Theory] [Theory]
@ -180,17 +173,15 @@ public class DrawImageTests
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, 130)] [WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, 130)]
public void NonOverlappingImageThrows(TestImageProvider<Rgba32> provider, int x, int y) public void NonOverlappingImageThrows(TestImageProvider<Rgba32> provider, int x, int y)
{ {
using (Image<Rgba32> background = provider.GetImage()) using Image<Rgba32> background = provider.GetImage();
using (var overlay = new Image<Rgba32>(Configuration.Default, 10, 10, Color.Black)) using Image<Rgba32> overlay = new(Configuration.Default, 10, 10, Color.Black);
{ ImageProcessingException ex = Assert.Throws<ImageProcessingException>(Test);
ImageProcessingException ex = Assert.Throws<ImageProcessingException>(Test);
Assert.Contains("does not overlap", ex.ToString()); Assert.Contains("does not overlap", ex.ToString());
void Test() void Test()
{ {
background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions())); 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()) using (Image<TPixel> image = provider.GetImage())
{ {
var encoder = new GifEncoder GifEncoder encoder = new()
{ {
// Use the palette quantizer without dithering to ensure results // Use the palette quantizer without dithering to ensure results
// are consistent // are consistent
@ -59,59 +59,45 @@ public class GifEncoderTests
// Compare encoded result // Compare encoded result
string path = provider.Utility.GetTestOutputFileName("gif", null, true); string path = provider.Utility.GetTestOutputFileName("gif", null, true);
using (var encoded = Image.Load<Rgba32>(path)) using Image<Rgba32> encoded = Image.Load<Rgba32>(path);
{ encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif");
encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif");
}
} }
[Theory] [Theory]
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{ {
var options = new GifEncoder(); GifEncoder options = new();
var testFile = TestFile.Create(imagePath); TestFile testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image()) using Image<Rgba32> input = testFile.CreateRgba32Image();
{ using MemoryStream memStream = new();
using (var memStream = new MemoryStream()) input.Save(memStream, options);
{
input.Save(memStream, options); memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
memStream.Position = 0; ImageMetadata meta = output.Metadata;
using (var output = Image.Load<Rgba32>(memStream)) Assert.Equal(xResolution, meta.HorizontalResolution);
{ Assert.Equal(yResolution, meta.VerticalResolution);
ImageMetadata meta = output.Metadata; Assert.Equal(resolutionUnit, meta.ResolutionUnits);
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
} }
[Fact] [Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() 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 Image<Rgba32> input = testFile.CreateRgba32Image();
{ using MemoryStream memStream = new();
using (var memStream = new MemoryStream()) input.Save(memStream, options);
{
input.Save(memStream, options); memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
memStream.Position = 0; GifMetadata metadata = output.Metadata.GetGifMetadata();
using (var output = Image.Load<Rgba32>(memStream)) Assert.Equal(1, metadata.Comments.Count);
{ Assert.Equal("ImageSharp", metadata.Comments[0]);
GifMetadata metadata = output.Metadata.GetGifMetadata();
Assert.Equal(1, metadata.Comments.Count);
Assert.Equal("ImageSharp", metadata.Comments[0]);
}
}
}
} }
[Theory] [Theory]
@ -119,25 +105,28 @@ public class GifEncoderTests
public void EncodeGlobalPaletteReturnsSmallerFile<TPixel>(TestImageProvider<TPixel> provider) public void EncodeGlobalPaletteReturnsSmallerFile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> 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. // Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global"); provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global");
encoder.ColorTableMode = GifColorTableMode.Local; encoder = new()
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local"); {
ColorTableMode = GifColorTableMode.Local,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }),
};
var fileInfoGlobal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "global")); provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local");
var fileInfoLocal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "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] [Theory]
@ -152,10 +141,10 @@ public class GifEncoderTests
{ {
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new GifEncoder() GifEncoder encoder = new()
{ {
ColorTableMode = GifColorTableMode.Global, ColorTableMode = GifColorTableMode.Global,
GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) PixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio)
}; };
string testOutputFile = provider.Utility.SaveTestOutputFile( string testOutputFile = provider.Utility.SaveTestOutputFile(
@ -166,8 +155,7 @@ public class GifEncoderTests
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);
// TODO: For proper regression testing of gifs, use a multi-frame reference output, or find a working reference decoder. // TODO: For proper regression testing of gifs, use a multi-frame reference output, or find a working reference decoder.
// IImageDecoder referenceDecoder = TestEnvironment.Ge // IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(testOutputFile);
// ReferenceDecoder(testOutputFile);
// using var encoded = Image.Load<TPixel>(testOutputFile, referenceDecoder); // using var encoded = Image.Load<TPixel>(testOutputFile, referenceDecoder);
// ValidatorComparer.VerifySimilarity(image, encoded); // ValidatorComparer.VerifySimilarity(image, encoded);
} }
@ -175,44 +163,42 @@ public class GifEncoderTests
[Fact] [Fact]
public void NonMutatingEncodePreservesPaletteCount() public void NonMutatingEncodePreservesPaletteCount()
{ {
using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes)) using MemoryStream inStream = new(TestFile.Create(TestImages.Gif.Leo).Bytes);
using (var outStream = new MemoryStream()) 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; ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
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 })
};
image.Save(outStream, encoder); image.Save(outStream, encoder);
outStream.Position = 0; outStream.Position = 0;
outStream.Position = 0; outStream.Position = 0;
var clone = Image.Load<Rgba32>(outStream); Image<Rgba32> clone = Image.Load<Rgba32>(outStream);
GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata(); GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata();
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors. // Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetadata.ColorTableLength); Assert.Equal(64, frameMetadata.ColorTableLength);
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
}
image.Dispose(); Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
clone.Dispose(); 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() public void HeaderChunk_ComesFirst()
{ {
// arrange // arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
@ -25,8 +25,8 @@ public partial class PngEncoderTests
// assert // assert
memStream.Position = 0; memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.Equal(PngChunkType.Header, type); Assert.Equal(PngChunkType.Header, type);
} }
@ -34,9 +34,9 @@ public partial class PngEncoderTests
public void EndChunk_IsLast() public void EndChunk_IsLast()
{ {
// arrange // arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
@ -47,15 +47,15 @@ public partial class PngEncoderTests
bool endChunkFound = false; bool endChunkFound = false;
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(endChunkFound); Assert.False(endChunkFound);
if (type == PngChunkType.End) if (type == PngChunkType.End)
{ {
endChunkFound = true; 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) public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
{ {
// arrange // arrange
var chunkType = (PngChunkType)chunkTypeObj; PngChunkType chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
@ -83,8 +83,8 @@ public partial class PngEncoderTests
bool dataFound = false; bool dataFound = false;
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type) if (chunkType == type)
{ {
Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk"); Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
@ -100,7 +100,7 @@ public partial class PngEncoderTests
break; 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) public void Chunk_ComesBeforeIDat(object chunkTypeObj)
{ {
// arrange // arrange
var chunkType = (PngChunkType)chunkTypeObj; PngChunkType chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
@ -124,8 +124,8 @@ public partial class PngEncoderTests
bool dataFound = false; bool dataFound = false;
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type) if (chunkType == type)
{ {
Assert.False(dataFound, $"{chunkType} chunk should come before data chunk"); Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
@ -136,7 +136,7 @@ public partial class PngEncoderTests
dataFound = true; 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() public void IgnoreMetadata_WillExcludeAllAncillaryChunks()
{ {
// arrange // arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 }; PngEncoder encoder = new() { SkipMetadata = true, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>() Dictionary<PngChunkType, bool> expectedChunkTypes = new()
{ {
{ PngChunkType.Header, false }, { PngChunkType.Header, false },
{ PngChunkType.Palette, false }, { PngChunkType.Palette, false },
{ PngChunkType.Data, false }, { PngChunkType.Data, false },
{ PngChunkType.End, false } { PngChunkType.End, false }
}; };
var excludedChunkTypes = new List<PngChunkType>() List<PngChunkType> excludedChunkTypes = new()
{ {
PngChunkType.Gamma, PngChunkType.Gamma,
PngChunkType.Exif, PngChunkType.Exif,
@ -174,15 +174,15 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded"); Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType)) if (expectedChunkTypes.ContainsKey(chunkType))
{ {
expectedChunkTypes[chunkType] = true; 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. // 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) public void ExcludeFilter_Works(object filterObj)
{ {
// arrange // arrange
var chunkFilter = (PngChunkFilter)filterObj; PngChunkFilter chunkFilter = (PngChunkFilter)filterObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 }; PngEncoder encoder = new() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>() Dictionary<PngChunkType, bool> expectedChunkTypes = new()
{ {
{ PngChunkType.Header, false }, { PngChunkType.Header, false },
{ PngChunkType.Gamma, false }, { PngChunkType.Gamma, false },
@ -219,7 +219,7 @@ public partial class PngEncoderTests
{ PngChunkType.Data, false }, { PngChunkType.Data, false },
{ PngChunkType.End, false } { PngChunkType.End, false }
}; };
var excludedChunkTypes = new List<PngChunkType>(); List<PngChunkType> excludedChunkTypes = new();
switch (chunkFilter) switch (chunkFilter)
{ {
case PngChunkFilter.ExcludeGammaChunk: case PngChunkFilter.ExcludeGammaChunk:
@ -267,15 +267,15 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded"); Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType)) if (expectedChunkTypes.ContainsKey(chunkType))
{ {
expectedChunkTypes[chunkType] = true; 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. // all expected chunk types should have been seen at least once.
@ -289,11 +289,11 @@ public partial class PngEncoderTests
public void ExcludeFilter_WithNone_DoesNotExcludeChunks() public void ExcludeFilter_WithNone_DoesNotExcludeChunks()
{ {
// arrange // arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 }; PngEncoder encoder = new() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
var expectedChunkTypes = new List<PngChunkType>() List<PngChunkType> expectedChunkTypes = new()
{ {
PngChunkType.Header, PngChunkType.Header,
PngChunkType.Gamma, PngChunkType.Gamma,
@ -314,11 +314,11 @@ public partial class PngEncoderTests
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0) while (bytesSpan.Length > 0)
{ {
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]);
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.True(expectedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been present"); 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;
using SixLabors.ImageSharp.Formats.Tiff.Writers; using SixLabors.ImageSharp.Formats.Tiff.Writers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
[Trait("Format", "Tiff")] [Trait("Format", "Tiff")]
public class TiffEncoderHeaderTests public class TiffEncoderHeaderTests
{ {
private static readonly MemoryAllocator MemoryAllocator = MemoryAllocator.Create(); private static readonly TiffEncoder Encoder = new();
private static readonly Configuration Configuration = Configuration.Default;
private static readonly ITiffEncoderOptions Options = new TiffEncoder();
[Fact] [Fact]
public void WriteHeader_WritesValidHeader() public void WriteHeader_WritesValidHeader()
{ {
using var stream = new MemoryStream(); using MemoryStream stream = new();
var encoder = new TiffEncoderCore(Options, MemoryAllocator); TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator);
using (var writer = new TiffStreamWriter(stream)) using (TiffStreamWriter writer = new(stream))
{ {
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); long firstIfdMarker = TiffEncoderCore.WriteHeader(writer);
} }
@ -31,13 +28,11 @@ public class TiffEncoderHeaderTests
[Fact] [Fact]
public void WriteHeader_ReturnsFirstIfdMarker() public void WriteHeader_ReturnsFirstIfdMarker()
{ {
using var stream = new MemoryStream(); using MemoryStream stream = new();
var encoder = new TiffEncoderCore(Options, MemoryAllocator); TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator);
using (var writer = new TiffStreamWriter(stream)) using TiffStreamWriter writer = new(stream);
{ long firstIfdMarker = TiffEncoderCore.WriteHeader(writer);
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); Assert.Equal(4, firstIfdMarker);
Assert.Equal(4, firstIfdMarker);
}
} }
} }

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

@ -14,9 +14,7 @@ public abstract partial class ImageFrameCollectionTests
{ {
[Fact] [Fact]
public void Constructor_ShouldCreateOneFrame() public void Constructor_ShouldCreateOneFrame()
{ => Assert.Equal(1, this.Collection.Count);
Assert.Equal(1, this.Collection.Count);
}
[Fact] [Fact]
public void AddNewFrame_FramesMustHaveSameSize() public void AddNewFrame_FramesMustHaveSameSize()
@ -24,7 +22,7 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>( 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); using ImageFrame<Rgba32> addedFrame = this.Collection.AddFrame(frame);
}); });
@ -75,7 +73,7 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>( 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); using ImageFrame<Rgba32> insertedFrame = this.Collection.InsertFrame(1, frame);
}); });
@ -100,8 +98,8 @@ public abstract partial class ImageFrameCollectionTests
ArgumentException ex = Assert.Throws<ArgumentException>( ArgumentException ex = Assert.Throws<ArgumentException>(
() => () =>
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 1, 1); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 1, 1);
new ImageFrameCollection<Rgba32>( new ImageFrameCollection<Rgba32>(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -113,8 +111,8 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void RemoveAtFrame_ThrowIfRemovingLastFrame() public void RemoveAtFrame_ThrowIfRemovingLastFrame()
{ {
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame }); new[] { imageFrame });
@ -126,9 +124,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist() public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist()
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -139,9 +137,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void RootFrameIsFrameAtIndexZero() public void RootFrameIsFrameAtIndexZero()
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -151,9 +149,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void ConstructorPopulatesFrames() public void ConstructorPopulatesFrames()
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -163,9 +161,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void DisposeClearsCollection() public void DisposeClearsCollection()
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -177,9 +175,9 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void Dispose_DisposesAllInnerFrames() public void Dispose_DisposesAllInnerFrames()
{ {
using var imageFrame1 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame1 = new(Configuration.Default, 10, 10);
using var imageFrame2 = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame<Rgba32> imageFrame2 = new(Configuration.Default, 10, 10);
var collection = new ImageFrameCollection<Rgba32>( ImageFrameCollection<Rgba32> collection = new(
this.Image, this.Image,
new[] { imageFrame1, imageFrame2 }); new[] { imageFrame1, imageFrame2 });
@ -188,11 +186,8 @@ public abstract partial class ImageFrameCollectionTests
Assert.All( Assert.All(
framesSnapShot, framesSnapShot,
f => f => // The pixel source of the frame is null after its been disposed.
{ Assert.Null(f.PixelBuffer));
// The pixel source of the frame is null after its been disposed.
Assert.Null(f.PixelBuffer);
});
} }
[Theory] [Theory]
@ -200,18 +195,14 @@ public abstract partial class ImageFrameCollectionTests
public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider) public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> img = provider.GetImage()) using Image<TPixel> img = provider.GetImage();
{ using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway using Image<TPixel> cloned = img.Frames.CloneFrame(0);
using (Image<TPixel> cloned = img.Frames.CloneFrame(0)) Assert.Equal(2, img.Frames.Count);
{ Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
Assert.Equal(2, img.Frames.Count);
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem)); cloned.ComparePixelBufferTo(imgMem);
cloned.ComparePixelBufferTo(imgMem);
}
}
} }
[Theory] [Theory]
@ -219,19 +210,15 @@ public abstract partial class ImageFrameCollectionTests
public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider) public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> img = provider.GetImage()) using Image<TPixel> img = provider.GetImage();
{ Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMemory));
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMemory)); TPixel[] sourcePixelData = imgMemory.ToArray();
TPixel[] sourcePixelData = imgMemory.ToArray();
using ImageFrame<Rgba32> imageFrame = new(Configuration.Default, 10, 10);
using var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10); using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame);
using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); using Image<TPixel> cloned = img.Frames.ExportFrame(0);
using (Image<TPixel> cloned = img.Frames.ExportFrame(0)) Assert.Equal(1, img.Frames.Count);
{ cloned.ComparePixelBufferTo(sourcePixelData.AsSpan());
Assert.Equal(1, img.Frames.Count);
cloned.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
}
} }
[Fact] [Fact]
@ -266,7 +253,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void AddFrame_clones_sourceFrame() 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); using ImageFrame<Rgba32> addedFrame = this.Image.Frames.AddFrame(otherFrame);
Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem)); Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem));
@ -277,7 +264,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void InsertFrame_clones_sourceFrame() 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); using ImageFrame<Rgba32> addedFrame = this.Image.Frames.InsertFrame(0, otherFrame);
Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem)); Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> otherFrameMem));
@ -332,7 +319,7 @@ public abstract partial class ImageFrameCollectionTests
this.Image.Frames.CreateFrame(); 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)); Assert.False(this.Image.Frames.Contains(frame));
} }
@ -343,14 +330,13 @@ public abstract partial class ImageFrameCollectionTests
configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = 1000 }; configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = 1000 };
configuration.PreferContiguousImageBuffers = true; 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.CreateFrame();
image.Frames.InsertFrame(0, image.Frames[0]); image.Frames.InsertFrame(0, image.Frames[0]);
image.Frames.CreateFrame(Color.Red); image.Frames.CreateFrame(Color.Red);
Assert.Equal(4, image.Frames.Count); Assert.Equal(4, image.Frames.Count);
IEnumerable<ImageFrame<Rgba32>> frames = image.Frames; foreach (ImageFrame<Rgba32> frame in image.Frames)
foreach (ImageFrame<Rgba32> frame in frames)
{ {
Assert.True(frame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> _)); Assert.True(frame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> _));
} }
@ -359,8 +345,8 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void DisposeCall_NoThrowIfCalledMultiple() public void DisposeCall_NoThrowIfCalledMultiple()
{ {
var image = new Image<Rgba32>(Configuration.Default, 10, 10); Image<Rgba32> image = new(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection; ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well image.Dispose(); // this should invalidate underlying collection as well
frameCollection.Dispose(); frameCollection.Dispose();
@ -369,33 +355,33 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void PublicProperties_ThrowIfDisposed() public void PublicProperties_ThrowIfDisposed()
{ {
var image = new Image<Rgba32>(Configuration.Default, 10, 10); Image<Rgba32> image = new(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection; ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.RootFrame; }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame prop = frameCollection.RootFrame; });
} }
[Fact] [Fact]
public void PublicMethods_ThrowIfDisposed() public void PublicMethods_ThrowIfDisposed()
{ {
var image = new Image<Rgba32>(Configuration.Default, 10, 10); Image<Rgba32> image = new(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection; ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(default(ImageFrame<Rgba32>)); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); }); Assert.Throws<ObjectDisposedException>(() => { bool res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); }); Assert.Throws<ObjectDisposedException>(() => { IEnumerator<ImageFrame<Rgba32>> res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); }); Assert.Throws<ObjectDisposedException>(() => { int prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); }); Assert.Throws<ObjectDisposedException>(() => frameCollection.RemoveFrame(default));
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, 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] [Fact]
public void AddFrame_OfDifferentPixelType() public void AddFrame_OfDifferentPixelType()
{ {
using (var sourceImage = new Image<Bgra32>( using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(), this.Image.GetConfiguration(),
this.Image.Width, this.Image.Width,
this.Image.Height, this.Image.Height,
@ -32,7 +32,7 @@ public abstract partial class ImageFrameCollectionTests
Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray(); Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray();
Assert.Equal(2, this.Collection.Count); Assert.Equal(2, this.Collection.Count);
var actualFrame = (ImageFrame<Rgba32>)this.Collection[1]; ImageFrame<Rgba32> actualFrame = (ImageFrame<Rgba32>)this.Collection[1];
actualFrame.ComparePixelBufferTo(expectedAllBlue); actualFrame.ComparePixelBufferTo(expectedAllBlue);
} }
@ -40,7 +40,7 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void InsertFrame_OfDifferentPixelType() public void InsertFrame_OfDifferentPixelType()
{ {
using (var sourceImage = new Image<Bgra32>( using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(), this.Image.GetConfiguration(),
this.Image.Width, this.Image.Width,
this.Image.Height, this.Image.Height,
@ -53,25 +53,20 @@ public abstract partial class ImageFrameCollectionTests
Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray(); Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray();
Assert.Equal(2, this.Collection.Count); Assert.Equal(2, this.Collection.Count);
var actualFrame = (ImageFrame<Rgba32>)this.Collection[0]; ImageFrame<Rgba32> actualFrame = (ImageFrame<Rgba32>)this.Collection[0];
actualFrame.ComparePixelBufferTo(expectedAllBlue); actualFrame.ComparePixelBufferTo(expectedAllBlue);
} }
[Fact] [Fact]
public void Constructor_ShouldCreateOneFrame() public void Constructor_ShouldCreateOneFrame()
{ => Assert.Equal(1, this.Collection.Count);
Assert.Equal(1, this.Collection.Count);
}
[Fact] [Fact]
public void AddNewFrame_FramesMustHaveSameSize() public void AddNewFrame_FramesMustHaveSameSize()
{ {
ArgumentException ex = Assert.Throws<ArgumentException>( 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); 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() public void AddNewFrame_Frame_FramesNotBeNull()
{ {
ArgumentNullException ex = Assert.Throws<ArgumentNullException>( ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
() => () => this.Collection.AddFrame(null));
{
this.Collection.AddFrame(null);
});
Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
} }
@ -92,10 +84,7 @@ public abstract partial class ImageFrameCollectionTests
public void InsertNewFrame_FramesMustHaveSameSize() public void InsertNewFrame_FramesMustHaveSameSize()
{ {
ArgumentException ex = Assert.Throws<ArgumentException>( 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); 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() public void InsertNewFrame_FramesNotBeNull()
{ {
ArgumentNullException ex = Assert.Throws<ArgumentNullException>( 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); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
} }
@ -116,10 +102,7 @@ public abstract partial class ImageFrameCollectionTests
public void RemoveAtFrame_ThrowIfRemovingLastFrame() public void RemoveAtFrame_ThrowIfRemovingLastFrame()
{ {
InvalidOperationException ex = Assert.Throws<InvalidOperationException>( InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
() => () => this.Collection.RemoveFrame(0));
{
this.Collection.RemoveFrame(0);
});
Assert.Equal("Cannot remove last frame.", ex.Message); Assert.Equal("Cannot remove last frame.", ex.Message);
} }
@ -134,30 +117,24 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void RootFrameIsFrameAtIndexZero() public void RootFrameIsFrameAtIndexZero()
{ => Assert.Equal(this.Collection.RootFrame, this.Collection[0]);
Assert.Equal(this.Collection.RootFrame, this.Collection[0]);
}
[Theory] [Theory]
[WithTestPatternImages(10, 10, PixelTypes.Rgba32 | PixelTypes.Bgr24)] [WithTestPatternImages(10, 10, PixelTypes.Rgba32 | PixelTypes.Bgr24)]
public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider) public void CloneFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> img = provider.GetImage()) using Image<TPixel> img = provider.GetImage();
{ ImageFrameCollection nonGenericFrameCollection = img.Frames;
ImageFrameCollection nonGenericFrameCollection = img.Frames;
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10)); // add a frame anyway nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10)); // add a frame anyway
using (Image cloned = nonGenericFrameCollection.CloneFrame(0)) using Image cloned = nonGenericFrameCollection.CloneFrame(0);
{ Assert.Equal(2, img.Frames.Count);
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)); Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
expectedClone.ComparePixelBufferTo(imgMem); expectedClone.ComparePixelBufferTo(imgMem);
}
}
} }
[Theory] [Theory]
@ -165,22 +142,18 @@ public abstract partial class ImageFrameCollectionTests
public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider) public void ExtractFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> img = provider.GetImage()) using Image<TPixel> img = provider.GetImage();
{ Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem));
Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory<TPixel> imgMem)); TPixel[] sourcePixelData = imgMem.ToArray();
TPixel[] sourcePixelData = imgMem.ToArray();
ImageFrameCollection nonGenericFrameCollection = img.Frames; ImageFrameCollection nonGenericFrameCollection = img.Frames;
nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10)); nonGenericFrameCollection.AddFrame(new ImageFrame<TPixel>(Configuration.Default, 10, 10));
using (Image cloned = nonGenericFrameCollection.ExportFrame(0)) using Image cloned = nonGenericFrameCollection.ExportFrame(0);
{ Assert.Equal(1, img.Frames.Count);
Assert.Equal(1, img.Frames.Count);
var expectedClone = (Image<TPixel>)cloned; Image<TPixel> expectedClone = (Image<TPixel>)cloned;
expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan()); expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan());
}
}
} }
[Fact] [Fact]
@ -190,7 +163,7 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(2, this.Image.Frames.Count); 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)); frame.ComparePixelBufferTo(default(Rgba32));
} }
@ -202,7 +175,7 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(2, this.Image.Frames.Count); 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); frame.ComparePixelBufferTo(Color.HotPink);
} }
@ -210,132 +183,127 @@ public abstract partial class ImageFrameCollectionTests
[Fact] [Fact]
public void MoveFrame_LeavesFrameInCorrectLocation() public void MoveFrame_LeavesFrameInCorrectLocation()
{ {
for (var i = 0; i < 9; i++) for (int i = 0; i < 9; i++)
{ {
this.Image.Frames.CreateFrame(); this.Image.Frames.CreateFrame();
} }
var frame = this.Image.Frames[4]; ImageFrame frame = this.Image.Frames[4];
this.Image.Frames.MoveFrame(4, 7); this.Image.Frames.MoveFrame(4, 7);
var newIndex = this.Image.Frames.IndexOf(frame); int newIndex = this.Image.Frames.IndexOf(frame);
Assert.Equal(7, newIndex); Assert.Equal(7, newIndex);
} }
[Fact] [Fact]
public void IndexOf_ReturnsCorrectIndex() public void IndexOf_ReturnsCorrectIndex()
{ {
for (var i = 0; i < 9; i++) for (int i = 0; i < 9; i++)
{ {
this.Image.Frames.CreateFrame(); this.Image.Frames.CreateFrame();
} }
var frame = this.Image.Frames[4]; ImageFrame frame = this.Image.Frames[4];
var index = this.Image.Frames.IndexOf(frame); int index = this.Image.Frames.IndexOf(frame);
Assert.Equal(4, index); Assert.Equal(4, index);
} }
[Fact] [Fact]
public void Contains_TrueIfMember() public void Contains_TrueIfMember()
{ {
for (var i = 0; i < 9; i++) for (int i = 0; i < 9; i++)
{ {
this.Image.Frames.CreateFrame(); this.Image.Frames.CreateFrame();
} }
var frame = this.Image.Frames[4]; ImageFrame frame = this.Image.Frames[4];
Assert.True(this.Image.Frames.Contains(frame)); Assert.True(this.Image.Frames.Contains(frame));
} }
[Fact] [Fact]
public void Contains_FalseIfNonMember() public void Contains_FalseIfNonMember()
{ {
for (var i = 0; i < 9; i++) for (int i = 0; i < 9; i++)
{ {
this.Image.Frames.CreateFrame(); 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)); Assert.False(this.Image.Frames.Contains(frame));
} }
[Fact] [Fact]
public void PublicProperties_ThrowIfDisposed() public void PublicProperties_ThrowIfDisposed()
{ {
var image = new Image<Rgba32>(Configuration.Default, 10, 10); Image<Rgba32> image = new(Configuration.Default, 10, 10);
var frameCollection = image.Frames; ImageFrameCollection<Rgba32> frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well 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] [Fact]
public void PublicMethods_ThrowIfDisposed() public void PublicMethods_ThrowIfDisposed()
{ {
var image = new Image<Rgba32>(Configuration.Default, 10, 10); Image<Rgba32> image = new(Configuration.Default, 10, 10);
var frameCollection = image.Frames; ImageFrameCollection<Rgba32> frameCollection = image.Frames;
var rgba32Array = new Rgba32[0]; Rgba32[] rgba32Array = Array.Empty<Rgba32>();
image.Dispose(); // this should invalidate underlying collection as well image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame)null); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame res = frameCollection.AddFrame((ImageFrame)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(rgba32Array); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame<Rgba32>)null); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame((ImageFrame<Rgba32>)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array.AsSpan()); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.AddFrame(rgba32Array.AsSpan()); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); }); Assert.Throws<ObjectDisposedException>(() => { bool res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); }); Assert.Throws<ObjectDisposedException>(() => { Image<Rgba32> res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); }); Assert.Throws<ObjectDisposedException>(() => { IEnumerator<ImageFrame<Rgba32>> res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); }); Assert.Throws<ObjectDisposedException>(() => { int prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); }); Assert.Throws<ObjectDisposedException>(() => { ImageFrame<Rgba32> prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); }); Assert.Throws<ObjectDisposedException>(() => frameCollection.RemoveFrame(default));
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, default); }); Assert.Throws<ObjectDisposedException>(() => frameCollection.MoveFrame(default, default));
} }
/// <summary> /// <summary>
/// Integration test for end-to end API validation. /// Integration test for end-to end API validation.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam> /// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="provider">The test image provider</param>
[Theory] [Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void ConstructGif_FromDifferentPixelTypes<TPixel>(TestImageProvider<TPixel> provider) public void ConstructGif_FromDifferentPixelTypes<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image source = provider.GetImage()) using Image source = provider.GetImage();
using (var dest = new Image<TPixel>(source.GetConfiguration(), source.Width, source.Height)) 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 CompareGifMetadata(source.Frames[i], dest.Frames[i]);
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]);
}
} }
} }
private static void ImportFrameAs<TPixel>(ImageFrameCollection source, ImageFrameCollection destination, int index) private static void ImportFrameAs<TPixel>(ImageFrameCollection source, ImageFrameCollection destination, int index)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image temp = source.CloneFrame(index)) using Image temp = source.CloneFrame(index);
{ using Image<TPixel> temp2 = temp.CloneAs<TPixel>();
using (Image<TPixel> temp2 = temp.CloneAs<TPixel>()) destination.AddFrame(temp2.Frames.RootFrame);
{
destination.AddFrame(temp2.Frames.RootFrame);
}
}
} }
private static void CompareGifMetadata(ImageFrame a, ImageFrame b) 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 class PixelSamplingStrategyTests
{ {
public static readonly TheoryData<int, int, int, int> DefaultPixelSamplingStrategy_Data = new TheoryData<int, int, int, int>() public static readonly TheoryData<int, int, int, int> DefaultPixelSamplingStrategy_MultiFrame_Data = new()
{ {
{ 100, 100, 1, 10000 }, { 100, 100, 1, 10000 },
{ 100, 100, 1, 5000 }, { 100, 100, 1, 5000 },
{ 100, 100, 10, 50000 }, { 100, 100, 10, 50000 },
{ 99, 100, 11, 30000 }, { 99, 100, 11, 30000 },
{ 97, 99, 11, 80000 }, { 97, 99, 11, 80000 },
{ 99, 100, 11, 20000 }, { 99, 100, 11, 20000 },
{ 99, 501, 20, 100000 }, { 99, 501, 20, 100000 },
{ 97, 500, 20, 10000 }, { 97, 500, 20, 10000 },
{ 103, 501, 20, 1000 }, { 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] [Fact]
public void ExtensivePixelSamplingStrategy_EnumeratesAll() public void ExtensivePixelSamplingStrategy_EnumeratesAll_MultiFrame()
{ {
using Image<L8> image = CreateTestImage(100, 100, 100); using Image<L8> image = CreateTestImage(100, 100, 100);
var strategy = new ExtensivePixelSamplingStrategy(); ExtensivePixelSamplingStrategy strategy = new();
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image)) foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image))
{ {
@ -39,13 +51,32 @@ public class PixelSamplingStrategyTests
ImageComparer.Exact.VerifySimilarity(expected, image); 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] [Theory]
[WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)] [WithBlankImages(nameof(DefaultPixelSamplingStrategy_MultiFrame_Data), 1, 1, PixelTypes.L8)]
public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider<L8> dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels) public void DefaultPixelSamplingStrategy_IsFair_MultiFrame(TestImageProvider<L8> dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels)
{ {
using Image<L8> image = CreateTestImage(width, height, noOfFrames); using Image<L8> image = CreateTestImage(width, height, noOfFrames);
var strategy = new DefaultPixelSamplingStrategy(maximumNumberOfPixels, 0.1); DefaultPixelSamplingStrategy strategy = new(maximumNumberOfPixels, 0.1);
long visitedPixels = 0; long visitedPixels = 0;
foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image)) foreach (Buffer2DRegion<L8> region in strategy.EnumeratePixelRegions(image))
@ -67,9 +98,40 @@ public class PixelSamplingStrategyTests
Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}"); 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) private static void PaintWhite(Buffer2DRegion<L8> region)
{ {
var white = new L8(255); L8 white = new(255);
for (int y = 0; y < region.Height; y++) for (int y = 0; y < region.Height; y++)
{ {
region.DangerousGetRowSpan(y).Fill(white); 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) private static Image<L8> CreateTestImage(int width, int height, int noOfFrames, bool paintWhite = false)
{ {
L8 bg = paintWhite ? new L8(255) : default; 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++) 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;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; 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. /// A Png encoder that uses the ImageSharp core encoder but the default configuration.
/// This allows encoding under environments with restricted memory. /// This allows encoding under environments with restricted memory.
/// </summary> /// </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> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param> /// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public override void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator; 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); 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="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public override async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Configuration configuration = Configuration.Default; Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator; 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 // 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 // IDisposable means you must use async/await, where the compiler generates the
// state machine and a continuation. // 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); await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
} }
} }

Loading…
Cancel
Save