diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs deleted file mode 100644 index 27d86f9218..0000000000 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ /dev/null @@ -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; - -/// -/// The options available for manipulating the encoder pipeline. -/// -internal interface IPngEncoderOptions -{ - /// - /// Gets the number of bits per sample or per palette index (not per pixel). - /// Not all values are allowed for all values. - /// - PngBitDepth? BitDepth { get; } - - /// - /// Gets the color type. - /// - PngColorType? ColorType { get; } - - /// - /// Gets the filter method. - /// - PngFilterMethod? FilterMethod { get; } - - /// - /// Gets the compression level 1-9. - /// Defaults to . - /// - PngCompressionLevel CompressionLevel { get; } - - /// - /// Gets the threshold of characters in text metadata, when compression should be used. - /// - int TextCompressionThreshold { get; } - - /// - /// Gets the gamma value, that will be written the image. - /// - /// The gamma value of the image. - float? Gamma { get; } - - /// - /// Gets the quantizer for reducing the color count. - /// - IQuantizer Quantizer { get; } - - /// - /// Gets the transparency threshold. - /// - byte Threshold { get; } - - /// - /// Gets a value indicating whether this instance should write an Adam7 interlaced image. - /// - PngInterlaceMode? InterlaceMethod { get; } - - /// - /// 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. - /// - bool IgnoreMetadata { get; } - - /// - /// Gets the chunk filter method. This allows to filter ancillary chunks. - /// - PngChunkFilter? ChunkFilter { get; } - - /// - /// 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. - /// - PngTransparentColorMode TransparentColorMode { get; } -} diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index d769bcbc8d..2daa136c3d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,84 +2,91 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png; /// /// Image encoder for writing image data to a stream in png format. /// -public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions +public class PngEncoder : QuantizingImageEncoder { - /// - public PngBitDepth? BitDepth { get; set; } - - /// - public PngColorType? ColorType { get; set; } + /// + /// Initializes a new instance of the class. + /// + public PngEncoder() => - /// - public PngFilterMethod? FilterMethod { get; set; } + // We set the quantizer to null here to allow the underlying encoder to create a + // quantizer with options appropriate to the encoding bit depth. + this.Quantizer = null; - /// - 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 values. + /// + public PngBitDepth? BitDepth { get; init; } - /// - public int TextCompressionThreshold { get; set; } = 1024; + /// + /// Gets the color type. + /// + public PngColorType? ColorType { get; init; } - /// - public float? Gamma { get; set; } + /// + /// Gets the filter method. + /// + public PngFilterMethod? FilterMethod { get; init; } - /// - public IQuantizer Quantizer { get; set; } + /// + /// Gets the compression level 1-9. + /// Defaults to . + /// + public PngCompressionLevel CompressionLevel { get; init; } = PngCompressionLevel.DefaultCompression; - /// - public byte Threshold { get; set; } = byte.MaxValue; + /// + /// Gets the threshold of characters in text metadata, when compression should be used. + /// + public int TextCompressionThreshold { get; init; } = 1024; - /// - public PngInterlaceMode? InterlaceMethod { get; set; } + /// + /// Gets the gamma value, that will be written the image. + /// + /// The gamma value of the image. + public float? Gamma { get; init; } - /// - public PngChunkFilter? ChunkFilter { get; set; } + /// + /// Gets the transparency threshold. + /// + public byte Threshold { get; init; } = byte.MaxValue; - /// - public bool IgnoreMetadata { get; set; } + /// + /// Gets a value indicating whether this instance should write an Adam7 interlaced image. + /// + public PngInterlaceMode? InterlaceMethod { get; init; } - /// - public PngTransparentColorMode TransparentColorMode { get; set; } + /// + /// Gets the chunk filter method. This allows to filter ancillary chunks. + /// + public PngChunkFilter? ChunkFilter { get; init; } /// - /// Encodes the image to the specified stream from the . + /// 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. /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public PngTransparentColorMode TransparentColorMode { get; init; } + + /// + public override void Encode(Image image, Stream stream) { - using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) - { - encoder.Encode(image, stream); - } + using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this); + encoder.Encode(image, stream); } - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// + public override async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { // The introduction of a local variable that refers to an object the implements // IDisposable means you must use async/await, where the compiler generates the // state machine and a continuation. - using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) - { - await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); - } + using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this); + await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c45da6a825..4267580dec 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -12,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png; @@ -46,17 +48,42 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private readonly byte[] chunkDataBuffer = new byte[16]; /// - /// The encoder options + /// The encoder with options /// - private readonly PngEncoderOptions options; + private readonly PngEncoder encoder; /// - /// The bit depth. + /// The gamma value + /// + private float? gamma; + + /// + /// The color type. + /// + private PngColorType colorType; + + /// + /// The number of bits per sample or per palette index (not per pixel). /// private byte bitDepth; /// - /// 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. + /// + private PngFilterMethod filterMethod; + + /// + /// Gets the interlace mode. + /// + private PngInterlaceMode interlaceMode; + + /// + /// The chunk filter method. This allows to filter ancillary chunks. + /// + private PngChunkFilter chunkFilter; + + /// + /// A value indicating whether to use 16 bit encoding for supported color types. /// private bool use16Bit; @@ -95,12 +122,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The to use for buffer allocations. /// The configuration. - /// The options for influencing the encoder - public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoderOptions options) + /// The encoder with options. + public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; - this.options = options; + this.encoder = encoder; } /// @@ -122,16 +149,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); - PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); + this.DeduceOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); Image clonedImage = null; - bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear; + bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear; if (clearTransparency) { clonedImage = image.Clone(); ClearTransparentPixels(clonedImage); } - IndexedImageFrame quantized = this.CreateQuantizedImage(image, clonedImage); + IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage); stream.Write(PngConstants.HeaderBytes); @@ -171,6 +198,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable where TPixel : unmanaged, IPixel => image.ProcessPixelRows(accessor => { + // TODO: We should be able to speed this up with SIMD and masking. Rgba32 rgba32 = default; Rgba32 transparent = Color.Transparent; for (int y = 0; y < accessor.Height; y++) @@ -189,27 +217,28 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable }); /// - /// Creates the quantized image and sets calculates and sets the bit depth. + /// Creates the quantized image and calculates and sets the bit depth. /// /// The type of the pixel. /// The image to quantize. /// Cloned image with transparent pixels are changed to black. /// The quantized image. - private IndexedImageFrame CreateQuantizedImage(Image image, Image clonedImage) + private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth( + Image image, + Image clonedImage) where TPixel : unmanaged, IPixel { IndexedImageFrame quantized; - if (this.options.TransparentColorMode == PngTransparentColorMode.Clear) + if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear) { - quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage); - this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized); + quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage); } else { - quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); - this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized); + quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image); } + this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -223,23 +252,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span rawScanlineSpan = this.currentScanline.GetSpan(); ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan); - if (this.options.ColorType == PngColorType.Grayscale) + if (this.colorType == PngColorType.Grayscale) { if (this.use16Bit) { // 16 bit grayscale - using (IMemoryOwner luminanceBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) - { - Span luminanceSpan = luminanceBuffer.GetSpan(); - ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan); - PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); + using IMemoryOwner luminanceBuffer = this.memoryAllocator.Allocate(rowSpan.Length); + Span luminanceSpan = luminanceBuffer.GetSpan(); + ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan); + PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); - // Can't map directly to byte array as it's big-endian. - for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) - { - L16 luminance = Unsafe.Add(ref luminanceRef, x); - BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); - } + // Can't map directly to byte array as it's big-endian. + for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) + { + L16 luminance = Unsafe.Add(ref luminanceRef, x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); } } else if (this.bitDepth == 8) @@ -382,7 +409,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) where TPixel : unmanaged, IPixel { - switch (this.options.ColorType) + switch (this.colorType) { case PngColorType.Palette: @@ -413,7 +440,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Used for attempting optimized filtering. private void FilterPixelBytes(ref Span filter, ref Span attempt) { - switch (this.options.FilterMethod) + switch (this.filterMethod) { case PngFilterMethod.None: NoneFilter.Encode(this.currentScanline.GetSpan(), filter); @@ -495,7 +522,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { // Palette images don't compress well with adaptive filtering. // Nor do images comprising a single row. - if (this.options.ColorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8) + if (this.colorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8) { NoneFilter.Encode(this.currentScanline.GetSpan(), filter); return; @@ -543,10 +570,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable width: this.width, height: this.height, bitDepth: this.bitDepth, - colorType: this.options.ColorType.Value, + colorType: this.colorType, compressionMethod: 0, // None filterMethod: 0, - interlaceMethod: this.options.InterlaceMethod.Value); + interlaceMethod: this.interlaceMode); header.WriteTo(this.chunkDataBuffer); @@ -593,7 +620,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable byte alpha = rgba.A; Unsafe.Add(ref colorTableRef, i) = rgba.Rgb; - if (alpha > this.options.Threshold) + if (alpha > this.encoder.Threshold) { alpha = byte.MaxValue; } @@ -619,7 +646,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WritePhysicalChunk(Stream stream, ImageMetadata meta) { - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk) + if ((this.chunkFilter & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk) { return; } @@ -636,7 +663,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WriteExifChunk(Stream stream, ImageMetadata meta) { - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk) + if ((this.chunkFilter & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk) { return; } @@ -658,7 +685,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void WriteXmpChunk(Stream stream, ImageMetadata meta) { const int iTxtHeaderSize = 5; - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) { return; } @@ -731,7 +758,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WriteTextChunks(Stream stream, PngMetadata meta) { - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) { return; } @@ -754,7 +781,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { // Write iTXt chunk. byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); - byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold + byte[] textBytes = textData.Value.Length > this.encoder.TextCompressionThreshold ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) : PngConstants.TranslatedEncoding.GetBytes(textData.Value); @@ -768,7 +795,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable keywordBytes.CopyTo(outputBytes); int bytesWritten = keywordBytes.Length; outputBytes[bytesWritten++] = 0; - if (textData.Value.Length > this.options.TextCompressionThreshold) + if (textData.Value.Length > this.encoder.TextCompressionThreshold) { // Indicate that the text is compressed. outputBytes[bytesWritten++] = 1; @@ -788,7 +815,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable textBytes.CopyTo(outputBytes[bytesWritten..]); this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes); } - else if (textData.Value.Length > this.options.TextCompressionThreshold) + else if (textData.Value.Length > this.encoder.TextCompressionThreshold) { // Write zTXt chunk. byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value)); @@ -827,7 +854,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private byte[] GetZlibCompressedBytes(byte[] dataBytes) { using MemoryStream memoryStream = new(); - using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) + using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) { deflateStream.Write(dataBytes); } @@ -842,15 +869,15 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The containing image data. private void WriteGammaChunk(Stream stream) { - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk) + if ((this.chunkFilter & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk) { return; } - if (this.options.Gamma > 0) + if (this.gamma > 0) { // 4-byte unsigned integer of gamma * 100,000. - uint gammaValue = (uint)(this.options.Gamma * 100_000F); + uint gammaValue = (uint)(this.gamma * 100_000F); BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), gammaValue); @@ -924,9 +951,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable using (MemoryStream memoryStream = new()) { - using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) + using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) { - if (this.options.InterlaceMethod == PngInterlaceMode.Adam7) + if (this.interlaceMode == PngInterlaceMode.Adam7) { if (quantized != null) { @@ -1192,4 +1219,196 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable ref IMemoryOwner current = ref this.currentScanline; RuntimeUtility.Swap(ref prev, ref current); } + + /// + /// Adjusts the options based upon the given metadata. + /// + /// The type of pixel format. + /// The encoder with options. + /// The PNG metadata. + /// if set to true [use16 bit]. + /// The bytes per pixel. + private void DeduceOptions( + PngEncoder encoder, + PngMetadata pngMetadata, + out bool use16Bit, + out int bytesPerPixel) + where TPixel : unmanaged, IPixel + { + // 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(); + 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()); + 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; + } + + /// + /// Creates the quantized frame. + /// + /// The type of the pixel. + /// The png encoder. + /// The color type. + /// The bits per component. + /// The image. + private static IndexedImageFrame CreateQuantizedFrame( + IQuantizingEncoderOptions encoder, + PngColorType colorType, + byte bitDepth, + Image image) + where TPixel : unmanaged, IPixel + { + 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 frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration()); + + frameQuantizer.BuildPalette(encoder.GlobalPixelSamplingStrategy, image); + return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); + } + + /// + /// Calculates the bit depth value. + /// + /// The type of the pixel. + /// The color type. + /// The bits per component. + /// The quantized frame. + /// Bit depth is not supported or not valid. + private static byte CalculateBitDepth( + PngColorType colorType, + byte bitDepth, + IndexedImageFrame quantizedFrame) + where TPixel : unmanaged, IPixel + { + 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; + } + + /// + /// Calculates the correct number of bytes per pixel for the given color type. + /// + /// The color type. + /// Whether to use 16 bits per component. + /// Bytes per pixel. + 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, + }; + + /// + /// Returns a suggested for the given + /// This is not exhaustive but covers many common pixel formats. + /// + /// The type of pixel format. + private static PngColorType SuggestColorType() + where TPixel : unmanaged, IPixel + => 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 + }; + + /// + /// Returns a suggested for the given + /// This is not exhaustive but covers many common pixel formats. + /// + /// The type of pixel format. + private static PngBitDepth SuggestBitDepth() + where TPixel : unmanaged, IPixel + => 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 + }; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs deleted file mode 100644 index 7fe27b78d7..0000000000 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ /dev/null @@ -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; - -/// -/// The options structure for the . -/// -internal class PngEncoderOptions : IPngEncoderOptions -{ - /// - /// Initializes a new instance of the class. - /// - /// The source. - 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; - } - - /// - public PngBitDepth? BitDepth { get; set; } - - /// - public PngColorType? ColorType { get; set; } - - /// - public PngFilterMethod? FilterMethod { get; set; } - - /// - public PngCompressionLevel CompressionLevel { get; } = PngCompressionLevel.DefaultCompression; - - /// - public int TextCompressionThreshold { get; } - - /// - public float? Gamma { get; set; } - - /// - public IQuantizer Quantizer { get; set; } - - /// - public byte Threshold { get; } - - /// - public PngInterlaceMode? InterlaceMethod { get; set; } - - /// - public PngChunkFilter? ChunkFilter { get; set; } - - /// - public bool IgnoreMetadata { get; set; } - - /// - public PngTransparentColorMode TransparentColorMode { get; set; } -} diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs deleted file mode 100644 index e1ee61c378..0000000000 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ /dev/null @@ -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; - -/// -/// The helper methods for the PNG encoder options. -/// -internal static class PngEncoderOptionsHelpers -{ - /// - /// Adjusts the options based upon the given metadata. - /// - /// The options. - /// The PNG metadata. - /// if set to true [use16 bit]. - /// The bytes per pixel. - public static void AdjustOptions( - PngEncoderOptions options, - PngMetadata pngMetadata, - out bool use16Bit, - out int bytesPerPixel) - where TPixel : unmanaged, IPixel - { - // 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(); - options.BitDepth ??= pngMetadata.BitDepth ?? SuggestBitDepth(); - 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; - } - } - - /// - /// Creates the quantized frame. - /// - /// The type of the pixel. - /// The options. - /// The image. - public static IndexedImageFrame CreateQuantizedFrame( - PngEncoderOptions options, - Image image) - where TPixel : unmanaged, IPixel - { - 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 frameQuantizer = options.Quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration())) - { - ImageFrame frame = image.Frames.RootFrame; - return frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); - } - } - - /// - /// Calculates the bit depth value. - /// - /// The type of the pixel. - /// The options. - /// The quantized frame. - public static byte CalculateBitDepth( - PngEncoderOptions options, - IndexedImageFrame quantizedFrame) - where TPixel : unmanaged, IPixel - { - 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; - } - - /// - /// Calculates the correct number of bytes per pixel for the given color type. - /// - /// Bytes per pixel. - 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, - }; - } - - /// - /// Returns a suggested for the given - /// This is not exhaustive but covers many common pixel formats. - /// - private static PngColorType SuggestColorType() - where TPixel : unmanaged, IPixel - { - 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 - }; - } - - /// - /// Returns a suggested for the given - /// This is not exhaustive but covers many common pixel formats. - /// - private static PngBitDepth SuggestBitDepth() - where TPixel : unmanaged, IPixel - { - 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 - }; - } -} diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs index 1129c4e27d..0dcb961f84 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs @@ -11,42 +11,40 @@ namespace SixLabors.ImageSharp.Tests.Drawing; [GroupOutput("Drawing")] public class DrawImageTests { - public static readonly TheoryData BlendingModes = new TheoryData - { - PixelColorBlendingMode.Normal, - PixelColorBlendingMode.Multiply, - PixelColorBlendingMode.Add, - PixelColorBlendingMode.Subtract, - PixelColorBlendingMode.Screen, - PixelColorBlendingMode.Darken, - PixelColorBlendingMode.Lighten, - PixelColorBlendingMode.Overlay, - PixelColorBlendingMode.HardLight, - }; + public static readonly TheoryData BlendingModes = new() + { + PixelColorBlendingMode.Normal, + PixelColorBlendingMode.Multiply, + PixelColorBlendingMode.Add, + PixelColorBlendingMode.Subtract, + PixelColorBlendingMode.Screen, + PixelColorBlendingMode.Darken, + PixelColorBlendingMode.Lighten, + PixelColorBlendingMode.Overlay, + PixelColorBlendingMode.HardLight, + }; [Theory] [WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)] public void ImageBlendingMatchesSvgSpecExamples(TestImageProvider provider, PixelColorBlendingMode mode) where TPixel : unmanaged, IPixel { - using (Image background = provider.GetImage()) - using (var source = Image.Load(TestFile.Create(TestImages.Png.Ducky).Bytes)) - { - background.Mutate(x => x.DrawImage(source, mode, 1F)); - background.DebugSave( - provider, - new { mode = mode }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - var comparer = ImageComparer.TolerantPercentage(0.01F); - background.CompareToReferenceOutput( - comparer, - provider, - new { mode = mode }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } + using Image background = provider.GetImage(); + using Image source = Image.Load(TestFile.Create(TestImages.Png.Ducky).Bytes); + background.Mutate(x => x.DrawImage(source, mode, 1F)); + background.DebugSave( + provider, + new { mode }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.01F); + background.CompareToReferenceOutput( + comparer, + provider, + new { mode }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] @@ -68,28 +66,29 @@ public class DrawImageTests float opacity) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - using (var blend = Image.Load(TestFile.Create(brushImage).Bytes)) + using Image image = provider.GetImage(); + using Image blend = Image.Load(TestFile.Create(brushImage).Bytes); + Size size = new(image.Width * 3 / 4, image.Height * 3 / 4); + Point position = new(image.Width / 8, image.Height / 8); + blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic)); + image.Mutate(x => x.DrawImage(blend, position, mode, opacity)); + FormattableString testInfo = $"{Path.GetFileNameWithoutExtension(brushImage)}-{mode}-{opacity}"; + + PngEncoder encoder; + if (provider.PixelType == PixelTypes.Rgba64) { - var size = new Size(image.Width * 3 / 4, image.Height * 3 / 4); - var position = new Point(image.Width / 8, image.Height / 8); - blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic)); - image.Mutate(x => x.DrawImage(blend, position, mode, opacity)); - FormattableString testInfo = $"{System.IO.Path.GetFileNameWithoutExtension(brushImage)}-{mode}-{opacity}"; - - var encoder = new PngEncoder(); - - if (provider.PixelType == PixelTypes.Rgba64) - { - encoder.BitDepth = PngBitDepth.Bit16; - } - - image.DebugSave(provider, testInfo, encoder: encoder); - image.CompareToReferenceOutput( - ImageComparer.TolerantPercentage(0.01f), - provider, - testInfo); + encoder = new() { BitDepth = PngBitDepth.Bit16 }; } + else + { + encoder = new(); + } + + image.DebugSave(provider, testInfo, encoder: encoder); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.01f), + provider, + testInfo); } [Theory] @@ -99,19 +98,17 @@ public class DrawImageTests { byte[] brushData = TestFile.Create(TestImages.Png.Ducky).Bytes; - using (Image image = provider.GetImage()) - using (Image brushImage = provider.PixelType == PixelTypes.Rgba32 - ? (Image)Image.Load(brushData) - : Image.Load(brushData)) - { - image.Mutate(c => c.DrawImage(brushImage, 0.5f)); - - image.DebugSave(provider, appendSourceFileOrDescription: false); - image.CompareToReferenceOutput( - ImageComparer.TolerantPercentage(0.01f), - provider, - appendSourceFileOrDescription: false); - } + using Image image = provider.GetImage(); + using Image brushImage = provider.PixelType == PixelTypes.Rgba32 + ? Image.Load(brushData) + : Image.Load(brushData); + image.Mutate(c => c.DrawImage(brushImage, 0.5f)); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.01f), + provider, + appendSourceFileOrDescription: false); } [Theory] @@ -121,26 +118,24 @@ public class DrawImageTests [WithSolidFilledImages(100, 100, "White", PixelTypes.Rgba32, -25, -30)] public void WorksWithDifferentLocations(TestImageProvider provider, int x, int y) { - using (Image background = provider.GetImage()) - using (var overlay = new Image(50, 50)) - { - Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory overlayMem)); - overlayMem.Span.Fill(Color.Black); - - background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F)); - - background.DebugSave( - provider, - testOutputDetails: $"{x}_{y}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - background.CompareToReferenceOutput( - provider, - testOutputDetails: $"{x}_{y}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } + using Image background = provider.GetImage(); + using Image overlay = new(50, 50); + Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory overlayMem)); + overlayMem.Span.Fill(Color.Black); + + background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F)); + + background.DebugSave( + provider, + testOutputDetails: $"{x}_{y}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + background.CompareToReferenceOutput( + provider, + testOutputDetails: $"{x}_{y}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] @@ -148,29 +143,27 @@ public class DrawImageTests public void DrawTransformed(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) - { - AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendRotationDegrees(45F) - .AppendScale(new SizeF(.25F, .25F)) - .AppendTranslation(new PointF(10, 10)); - - // Apply a background color so we can see the translation. - blend.Mutate(x => x.Transform(builder)); - blend.Mutate(x => x.BackgroundColor(Color.HotPink)); - - // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor - var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); - image.Mutate(x => x.DrawImage(blend, position, .75F)); - - image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); - image.CompareToReferenceOutput( - ImageComparer.TolerantPercentage(0.002f), - provider, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - } + using Image image = provider.GetImage(); + using Image blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes); + AffineTransformBuilder builder = new AffineTransformBuilder() + .AppendRotationDegrees(45F) + .AppendScale(new SizeF(.25F, .25F)) + .AppendTranslation(new PointF(10, 10)); + + // Apply a background color so we can see the translation. + blend.Mutate(x => x.Transform(builder)); + blend.Mutate(x => x.BackgroundColor(Color.HotPink)); + + // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor + Point position = new((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); + image.Mutate(x => x.DrawImage(blend, position, .75F)); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.002f), + provider, + appendSourceFileOrDescription: false, + appendPixelTypeToFileName: false); } [Theory] @@ -180,17 +173,15 @@ public class DrawImageTests [WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, 130)] public void NonOverlappingImageThrows(TestImageProvider provider, int x, int y) { - using (Image background = provider.GetImage()) - using (var overlay = new Image(Configuration.Default, 10, 10, Color.Black)) - { - ImageProcessingException ex = Assert.Throws(Test); + using Image background = provider.GetImage(); + using Image overlay = new(Configuration.Default, 10, 10, Color.Black); + ImageProcessingException ex = Assert.Throws(Test); - Assert.Contains("does not overlap", ex.ToString()); + Assert.Contains("does not overlap", ex.ToString()); - void Test() - { - background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions())); - } + void Test() + { + background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions())); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index d5365e8f73..83b8a88695 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -46,7 +46,7 @@ public class GifEncoderTests using (Image image = provider.GetImage()) { - var encoder = new GifEncoder + GifEncoder encoder = new() { // Use the palette quantizer without dithering to ensure results // are consistent @@ -59,59 +59,45 @@ public class GifEncoderTests // Compare encoded result string path = provider.Utility.GetTestOutputFileName("gif", null, true); - using (var encoded = Image.Load(path)) - { - encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif"); - } + using Image encoded = Image.Load(path); + encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif"); } [Theory] [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) { - var options = new GifEncoder(); - - var testFile = TestFile.Create(imagePath); - using (Image input = testFile.CreateRgba32Image()) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - ImageMetadata meta = output.Metadata; - Assert.Equal(xResolution, meta.HorizontalResolution); - Assert.Equal(yResolution, meta.VerticalResolution); - Assert.Equal(resolutionUnit, meta.ResolutionUnits); - } - } - } + GifEncoder options = new(); + + TestFile testFile = TestFile.Create(imagePath); + using Image input = testFile.CreateRgba32Image(); + using MemoryStream memStream = new(); + input.Save(memStream, options); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + ImageMetadata meta = output.Metadata; + Assert.Equal(xResolution, meta.HorizontalResolution); + Assert.Equal(yResolution, meta.VerticalResolution); + Assert.Equal(resolutionUnit, meta.ResolutionUnits); } [Fact] public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() { - var options = new GifEncoder(); + GifEncoder options = new(); - var testFile = TestFile.Create(TestImages.Gif.Rings); + TestFile testFile = TestFile.Create(TestImages.Gif.Rings); - using (Image input = testFile.CreateRgba32Image()) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - GifMetadata metadata = output.Metadata.GetGifMetadata(); - Assert.Equal(1, metadata.Comments.Count); - Assert.Equal("ImageSharp", metadata.Comments[0]); - } - } - } + using Image input = testFile.CreateRgba32Image(); + using MemoryStream memStream = new(); + input.Save(memStream, options); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + GifMetadata metadata = output.Metadata.GetGifMetadata(); + Assert.Equal(1, metadata.Comments.Count); + Assert.Equal("ImageSharp", metadata.Comments[0]); } [Theory] @@ -119,25 +105,28 @@ public class GifEncoderTests public void EncodeGlobalPaletteReturnsSmallerFile(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) + using Image image = provider.GetImage(); + GifEncoder encoder = new() { - var encoder = new GifEncoder - { - ColorTableMode = GifColorTableMode.Global, - Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) - }; + ColorTableMode = GifColorTableMode.Global, + Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) + }; - // Always save as we need to compare the encoded output. - provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global"); + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "gif", encoder, "global"); - encoder.ColorTableMode = GifColorTableMode.Local; - provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local"); + encoder = new() + { + ColorTableMode = GifColorTableMode.Local, + Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }), + }; - var fileInfoGlobal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "global")); - var fileInfoLocal = new FileInfo(provider.Utility.GetTestOutputFileName("gif", "local")); + provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local"); - Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length); - } + FileInfo fileInfoGlobal = new(provider.Utility.GetTestOutputFileName("gif", "global")); + FileInfo fileInfoLocal = new(provider.Utility.GetTestOutputFileName("gif", "local")); + + Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length); } [Theory] @@ -152,7 +141,7 @@ public class GifEncoderTests { using Image image = provider.GetImage(); - var encoder = new GifEncoder() + GifEncoder encoder = new() { ColorTableMode = GifColorTableMode.Global, GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) @@ -166,8 +155,7 @@ public class GifEncoderTests appendPixelTypeToFileName: false); // TODO: For proper regression testing of gifs, use a multi-frame reference output, or find a working reference decoder. - // IImageDecoder referenceDecoder = TestEnvironment.Ge - // ReferenceDecoder(testOutputFile); + // IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(testOutputFile); // using var encoded = Image.Load(testOutputFile, referenceDecoder); // ValidatorComparer.VerifySimilarity(image, encoded); } @@ -175,44 +163,42 @@ public class GifEncoderTests [Fact] public void NonMutatingEncodePreservesPaletteCount() { - using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes)) - using (var outStream = new MemoryStream()) + using MemoryStream inStream = new(TestFile.Create(TestImages.Gif.Leo).Bytes); + using MemoryStream outStream = new(); + inStream.Position = 0; + + Image image = Image.Load(inStream); + GifMetadata metaData = image.Metadata.GetGifMetadata(); + GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); + GifColorTableMode colorMode = metaData.ColorTableMode; + GifEncoder encoder = new() { - inStream.Position = 0; - - var image = Image.Load(inStream); - GifMetadata metaData = image.Metadata.GetGifMetadata(); - GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); - GifColorTableMode colorMode = metaData.ColorTableMode; - var encoder = new GifEncoder - { - ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) - }; + ColorTableMode = colorMode, + Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) + }; - image.Save(outStream, encoder); - outStream.Position = 0; + image.Save(outStream, encoder); + outStream.Position = 0; - outStream.Position = 0; - var clone = Image.Load(outStream); + outStream.Position = 0; + Image clone = Image.Load(outStream); - GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata(); - Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); + GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata(); + Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); - // Gifiddle and Cyotek GifInfo say this image has 64 colors. - Assert.Equal(64, frameMetadata.ColorTableLength); + // Gifiddle and Cyotek GifInfo say this image has 64 colors. + Assert.Equal(64, frameMetadata.ColorTableLength); - for (int i = 0; i < image.Frames.Count; i++) - { - GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); - GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata(); - - Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); - Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); - } + for (int i = 0; i < image.Frames.Count; i++) + { + GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); + GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata(); - image.Dispose(); - clone.Dispose(); + Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); + Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); } + + image.Dispose(); + clone.Dispose(); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 5a9b1df2a7..044da21938 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -15,9 +15,9 @@ public partial class PngEncoderTests public void HeaderChunk_ComesFirst() { // arrange - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, PngEncoder); @@ -25,8 +25,8 @@ public partial class PngEncoderTests // assert memStream.Position = 0; Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. - BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); Assert.Equal(PngChunkType.Header, type); } @@ -34,9 +34,9 @@ public partial class PngEncoderTests public void EndChunk_IsLast() { // arrange - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, PngEncoder); @@ -47,15 +47,15 @@ public partial class PngEncoderTests bool endChunkFound = false; while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); Assert.False(endChunkFound); if (type == PngChunkType.End) { endChunkFound = true; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } @@ -68,10 +68,10 @@ public partial class PngEncoderTests public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj) { // arrange - var chunkType = (PngChunkType)chunkTypeObj; - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + PngChunkType chunkType = (PngChunkType)chunkTypeObj; + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, PngEncoder); @@ -83,8 +83,8 @@ public partial class PngEncoderTests bool dataFound = false; while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); if (chunkType == type) { Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk"); @@ -100,7 +100,7 @@ public partial class PngEncoderTests break; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } @@ -110,10 +110,10 @@ public partial class PngEncoderTests public void Chunk_ComesBeforeIDat(object chunkTypeObj) { // arrange - var chunkType = (PngChunkType)chunkTypeObj; - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + PngChunkType chunkType = (PngChunkType)chunkTypeObj; + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, PngEncoder); @@ -124,8 +124,8 @@ public partial class PngEncoderTests bool dataFound = false; while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); if (chunkType == type) { Assert.False(dataFound, $"{chunkType} chunk should come before data chunk"); @@ -136,7 +136,7 @@ public partial class PngEncoderTests dataFound = true; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } @@ -144,18 +144,18 @@ public partial class PngEncoderTests public void IgnoreMetadata_WillExcludeAllAncillaryChunks() { // arrange - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 }; - var expectedChunkTypes = new Dictionary() + using MemoryStream memStream = new(); + PngEncoder encoder = new() { SkipMetadata = true, TextCompressionThreshold = 8 }; + Dictionary expectedChunkTypes = new() { { PngChunkType.Header, false }, { PngChunkType.Palette, false }, { PngChunkType.Data, false }, { PngChunkType.End, false } }; - var excludedChunkTypes = new List() + List excludedChunkTypes = new() { PngChunkType.Gamma, PngChunkType.Exif, @@ -174,15 +174,15 @@ public partial class PngEncoderTests Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded"); if (expectedChunkTypes.ContainsKey(chunkType)) { expectedChunkTypes[chunkType] = true; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } // all expected chunk types should have been seen at least once. @@ -201,12 +201,12 @@ public partial class PngEncoderTests public void ExcludeFilter_Works(object filterObj) { // arrange - var chunkFilter = (PngChunkFilter)filterObj; - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + PngChunkFilter chunkFilter = (PngChunkFilter)filterObj; + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 }; - var expectedChunkTypes = new Dictionary() + using MemoryStream memStream = new(); + PngEncoder encoder = new() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 }; + Dictionary expectedChunkTypes = new() { { PngChunkType.Header, false }, { PngChunkType.Gamma, false }, @@ -219,7 +219,7 @@ public partial class PngEncoderTests { PngChunkType.Data, false }, { PngChunkType.End, false } }; - var excludedChunkTypes = new List(); + List excludedChunkTypes = new(); switch (chunkFilter) { case PngChunkFilter.ExcludeGammaChunk: @@ -267,15 +267,15 @@ public partial class PngEncoderTests Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded"); if (expectedChunkTypes.ContainsKey(chunkType)) { expectedChunkTypes[chunkType] = true; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } // all expected chunk types should have been seen at least once. @@ -289,11 +289,11 @@ public partial class PngEncoderTests public void ExcludeFilter_WithNone_DoesNotExcludeChunks() { // arrange - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + TestFile testFile = TestFile.Create(TestImages.Png.PngWithMetadata); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 }; - var expectedChunkTypes = new List() + using MemoryStream memStream = new(); + PngEncoder encoder = new() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 }; + List expectedChunkTypes = new() { PngChunkType.Header, PngChunkType.Gamma, @@ -314,11 +314,11 @@ public partial class PngEncoderTests Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. while (bytesSpan.Length > 0) { - int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4)); - var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); Assert.True(expectedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been present"); - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs index 6ba73cd279..1290fdea3a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs @@ -4,8 +4,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -13,57 +11,20 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; /// A Png encoder that uses the ImageSharp core encoder but the default configuration. /// This allows encoding under environments with restricted memory. /// -public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder, IPngEncoderOptions +public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder { - /// - public PngBitDepth? BitDepth { get; set; } - - /// - public PngColorType? ColorType { get; set; } - - /// - public PngFilterMethod? FilterMethod { get; set; } - - /// - public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression; - - /// - public int TextCompressionThreshold { get; set; } = 1024; - - /// - public float? Gamma { get; set; } - - /// - public IQuantizer Quantizer { get; set; } - - /// - public byte Threshold { get; set; } = byte.MaxValue; - - /// - public PngInterlaceMode? InterlaceMethod { get; set; } - - /// - public PngChunkFilter? ChunkFilter { get; set; } - - /// - public bool IgnoreMetadata { get; set; } - - /// - public PngTransparentColorMode TransparentColorMode { get; set; } - /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { Configuration configuration = Configuration.Default; MemoryAllocator allocator = configuration.MemoryAllocator; - using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this)); + using PngEncoderCore encoder = new(allocator, configuration, this); encoder.Encode(image, stream); } @@ -75,8 +36,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder /// The to encode the image data to. /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { Configuration configuration = Configuration.Default; MemoryAllocator allocator = configuration.MemoryAllocator; @@ -84,7 +44,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder // The introduction of a local variable that refers to an object the implements // IDisposable means you must use async/await, where the compiler generates the // state machine and a continuation. - using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this)); + using PngEncoderCore encoder = new(allocator, configuration, this); await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); } }