Browse Source

Migrate PngEncoder and fix tests

pull/2269/head
James Jackson-South 4 years ago
parent
commit
4499df7487
  1. 77
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  2. 111
      src/ImageSharp/Formats/Png/PngEncoder.cs
  3. 315
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 68
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  5. 214
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  6. 221
      tests/ImageSharp.Tests/Drawing/DrawImageTests.cs
  7. 164
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  8. 90
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  9. 50
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs

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.DeduceOptions<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 DeduceOptions<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>(
IQuantizingEncoderOptions 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.GlobalPixelSamplingStrategy, 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) == -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>
/// <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
};
}
}

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

164
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,7 +141,7 @@ 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) GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio)
@ -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)..];
} }
} }
} }

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