diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 9c285e21de..0cf28c6bb2 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -57,6 +57,9 @@ internal static class AotCompilerTools /// If you are getting the above error, you need to call this method, which will pre-seed the AoT compiler with the /// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!! /// + /// + /// This method is used for AOT code generation only. Do not call it at runtime. + /// [Preserve] private static void SeedPixelFormats() { @@ -487,8 +490,10 @@ private static void AotCompileQuantizer() private static void AotCompilePixelSamplingStrategys() where TPixel : unmanaged, IPixel { - default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default); - default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default); + default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(Image)); + default(DefaultPixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame)); + default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(Image)); + default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame)); } /// @@ -513,13 +518,13 @@ private static void AotCompileDither() where TPixel : unmanaged, IPixel where TDither : struct, IDither { - var octree = default(OctreeQuantizer); + OctreeQuantizer octree = default; default(TDither).ApplyQuantizationDither, TPixel>(ref octree, default, default, default); - var palette = default(PaletteQuantizer); + PaletteQuantizer palette = default; default(TDither).ApplyQuantizationDither, TPixel>(ref palette, default, default, default); - var wu = default(WuQuantizer); + WuQuantizer wu = default; default(TDither).ApplyQuantizationDither, TPixel>(ref wu, default, default, default); default(TDither).ApplyPaletteDither.DitherProcessor, TPixel>(default, default, default); } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index a410a862b5..9e20da170a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -2,48 +2,38 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp; /// /// Image encoder for writing an image to a stream as a Windows bitmap. /// -public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions +public sealed class BmpEncoder : QuantizingImageEncoder { /// - /// Gets or sets the number of bits per pixel. + /// Gets the number of bits per pixel. /// - public BmpBitsPerPixel? BitsPerPixel { get; set; } + public BmpBitsPerPixel? BitsPerPixel { get; init; } /// - /// Gets or sets a value indicating whether the encoder should support transparency. + /// Gets a value indicating whether the encoder should support transparency. /// Note: Transparency support only works together with 32 bits per pixel. This option will /// change the default behavior of the encoder of writing a bitmap version 3 info header with no compression. /// Instead a bitmap version 4 info header will be written with the BITFIELDS compression. /// - public bool SupportTransparency { get; set; } - - /// - /// Gets or sets the quantizer for reducing the color count for 8-Bit images. - /// Defaults to Wu Quantizer. - /// - public IQuantizer Quantizer { get; set; } + public bool SupportTransparency { get; init; } /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { - var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); + BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); encoder.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); + BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 471e741826..a4e1f8eef9 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp; @@ -92,17 +91,23 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals /// private readonly IQuantizer quantizer; + /// + /// The pixel sampling strategy for quantization. + /// + private readonly IPixelSamplingStrategy pixelSamplingStrategy; + /// /// Initializes a new instance of the class. /// - /// The encoder options. + /// The encoder with options. /// The memory manager. - public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocator) + public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; - this.bitsPerPixel = options.BitsPerPixel; - this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; - this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; + this.bitsPerPixel = encoder.BitsPerPixel; + this.quantizer = encoder.Quantizer; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; } /// @@ -159,7 +164,7 @@ public void Encode(Image image, Stream stream, CancellationToken WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); - this.WriteImage(stream, image.Frames.RootFrame); + this.WriteImage(stream, image); WriteColorProfile(stream, iccProfileData, buffer); stream.Flush(); @@ -311,10 +316,10 @@ private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span /// /// The containing pixel data. /// - private void WriteImage(Stream stream, ImageFrame image) + private void WriteImage(Stream stream, Image image) where TPixel : unmanaged, IPixel { - Buffer2D pixels = image.PixelBuffer; + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; switch (this.bitsPerPixel) { case BmpBitsPerPixel.Pixel32: @@ -433,8 +438,8 @@ private void Write16BitPixelData(Stream stream, Buffer2D pixels) /// /// The type of the pixel. /// The to write to. - /// The containing pixel data. - private void Write8BitPixelData(Stream stream, ImageFrame image) + /// The containing pixel data. + private void Write8BitPixelData(Stream stream, Image image) where TPixel : unmanaged, IPixel { bool isL8 = typeof(TPixel) == typeof(L8); @@ -456,13 +461,15 @@ private void Write8BitPixelData(Stream stream, ImageFrame image) /// /// The type of the pixel. /// The to write to. - /// The containing pixel data. + /// The containing pixel data. /// A byte span of size 1024 for the color palette. - private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) + private void Write8BitColor(Stream stream, Image image, Span colorPalette) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration); - using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); @@ -486,7 +493,7 @@ private void Write8BitColor(Stream stream, ImageFrame image, Spa /// The to write to. /// The containing pixel data. /// A byte span of size 1024 for the color palette. - private void Write8BitPixelData(Stream stream, ImageFrame image, Span colorPalette) + private void Write8BitPixelData(Stream stream, Image image, Span colorPalette) where TPixel : unmanaged, IPixel { // Create a color palette with 256 different gray values. @@ -503,7 +510,7 @@ private void Write8BitPixelData(Stream stream, ImageFrame image, } stream.Write(colorPalette); - Buffer2D imageBuffer = image.PixelBuffer; + Buffer2D imageBuffer = image.GetRootFramePixelBuffer(); for (int y = image.Height - 1; y >= 0; y--) { ReadOnlySpan inputPixelRow = imageBuffer.DangerousGetRowSpan(y); @@ -523,14 +530,17 @@ private void Write8BitPixelData(Stream stream, ImageFrame image, /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write4BitPixelData(Stream stream, ImageFrame image) + private void Write4BitPixelData(Stream stream, Image image) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() { MaxColors = 16 }); - using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize4Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); @@ -567,14 +577,17 @@ private void Write4BitPixelData(Stream stream, ImageFrame image) /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write2BitPixelData(Stream stream, ImageFrame image) + private void Write2BitPixelData(Stream stream, Image image) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() { MaxColors = 4 }); - using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize2Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); @@ -620,14 +633,17 @@ private void Write2BitPixelData(Stream stream, ImageFrame image) /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write1BitPixelData(Stream stream, ImageFrame image) + private void Write1BitPixelData(Stream stream, Image image) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() { MaxColors = 2 }); - using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize1Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs deleted file mode 100644 index c2ce99ec71..0000000000 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors.Quantization; - -namespace SixLabors.ImageSharp.Formats.Bmp; - -/// -/// Configuration options for use during bmp encoding. -/// -internal interface IBmpEncoderOptions -{ - /// - /// Gets the number of bits per pixel. - /// - BmpBitsPerPixel? BitsPerPixel { get; } - - /// - /// Gets a value indicating whether the encoder should support transparency. - /// Note: Transparency support only works together with 32 bits per pixel. This option will - /// change the default behavior of the encoder of writing a bitmap version 3 info header with no compression. - /// Instead a bitmap version 4 info header will be written with the BITFIELDS compression. - /// - bool SupportTransparency { get; } - - /// - /// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images. - /// - IQuantizer Quantizer { get; } -} diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index b6441db102..351554eb07 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -2,47 +2,30 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif; /// /// Image encoder for writing image data to a stream in gif format. /// -public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions +public sealed class GifEncoder : QuantizingImageEncoder { /// - /// Gets or sets the quantizer for reducing the color count. - /// Defaults to the + /// Gets the color table mode: Global or local. /// - public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree; - - /// - /// Gets or sets the color table mode: Global or local. - /// - public GifColorTableMode? ColorTableMode { get; set; } - - /// - /// Gets or sets the used for quantization - /// when building a global color table in case of . - /// - public IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; set; } = new DefaultPixelSamplingStrategy(); + public GifColorTableMode? ColorTableMode { get; init; } /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { - var encoder = new GifEncoderCore(image.GetConfiguration(), this); + GifEncoderCore encoder = new(image.GetConfiguration(), this); encoder.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new GifEncoderCore(image.GetConfiguration(), this); + GifEncoderCore encoder = new(image.GetConfiguration(), this); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index cfd4ba36a6..14d20cf909 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -33,6 +33,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// private readonly byte[] buffer = new byte[20]; + /// + /// Whether to skip metadata during encode. + /// + private readonly bool skipMetadata; + /// /// The quantizer used to generate the color palette. /// @@ -57,14 +62,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The options for the encoder. - public GifEncoderCore(Configuration configuration, IGifEncoderOptions options) + /// The encoder with options. + public GifEncoderCore(Configuration configuration, GifEncoder encoder) { this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; - this.quantizer = options.Quantizer; - this.colorTableMode = options.ColorTableMode; - this.pixelSamplingStrategy = options.GlobalPixelSamplingStrategy; + this.skipMetadata = encoder.SkipMetadata; + this.quantizer = encoder.Quantizer; + this.colorTableMode = encoder.ColorTableMode; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; } /// @@ -97,7 +103,8 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds()); + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame); + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); } } @@ -116,12 +123,15 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteColorTable(quantized, stream); } - // Write the comments. - this.WriteComments(gifMetadata, stream); + if (!this.skipMetadata) + { + // Write the comments. + this.WriteComments(gifMetadata, stream); - // Write application extensions. - XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; - this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); + // Write application extensions. + XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); + } if (useGlobalTable) { diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs deleted file mode 100644 index 1ed68a9177..0000000000 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors.Quantization; - -namespace SixLabors.ImageSharp.Formats.Gif; - -/// -/// The configuration options used for encoding gifs. -/// -internal interface IGifEncoderOptions -{ - /// - /// Gets the quantizer used to generate the color palette. - /// - IQuantizer Quantizer { get; } - - /// - /// Gets the color table mode: Global or local. - /// - GifColorTableMode? ColorTableMode { get; } - - /// - /// Gets the used for quantization when building a global color table. - /// - IPixelSamplingStrategy GlobalPixelSamplingStrategy { get; } -} diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 42f15cf976..c05e0d83cb 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -58,7 +58,7 @@ internal static IImageInfo Identify( Stream stream, CancellationToken cancellationToken) { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); + using BufferedReadStream bufferedReadStream = new(configuration, stream); try { @@ -86,7 +86,7 @@ internal static Image Decode( CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); + using BufferedReadStream bufferedReadStream = new(configuration, stream); try { diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs new file mode 100644 index 0000000000..a0c087e646 --- /dev/null +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// The base class for all image encoders. +/// +public abstract class ImageEncoder : IImageEncoder +{ + /// + /// Gets a value indicating whether to ignore decoded metadata when encoding. + /// + public bool SkipMetadata { get; init; } + + /// + public abstract void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + public abstract Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; +} + +/// +/// The base class for all image encoders that allow color palette generation via quantization. +/// +public abstract class QuantizingImageEncoder : ImageEncoder +{ + /// + /// Gets the quantizer used to generate the color palette. + /// + public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree; + + /// + /// Gets the used for quantization when building color palettes. + /// + public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); +} diff --git a/src/ImageSharp/Formats/ImageEncoderUtilities.cs b/src/ImageSharp/Formats/ImageEncoderUtilities.cs index 94f74ea253..665431c952 100644 --- a/src/ImageSharp/Formats/ImageEncoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageEncoderUtilities.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; @@ -22,11 +22,11 @@ public static async Task EncodeAsync( } else { - using var ms = new MemoryStream(); + using MemoryStream ms = new(); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) - .ConfigureAwait(false); + .ConfigureAwait(false); } Task DoEncodeAsync(Stream innerStream) diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs deleted file mode 100644 index b7e6d5d614..0000000000 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jpeg; - -/// -/// Encoder for writing the data image to a stream in jpeg format. -/// -internal interface IJpegEncoderOptions -{ - /// - /// Gets or sets the quality, that will be used to encode the image. Quality - /// index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? Quality { get; set; } - - /// - /// Gets or sets the component encoding mode. - /// - /// - /// Interleaved encoding mode encodes all color components in a single scan. - /// Non-interleaved encoding mode encodes each color component in a separate scan. - /// - public bool? Interleaved { get; set; } - - /// - /// Gets or sets jpeg color for encoding. - /// - public JpegEncodingColor? ColorType { get; set; } -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 28d095eedc..2ddd829f87 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,25 +1,28 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.PixelFormats; - namespace SixLabors.ImageSharp.Formats.Jpeg; /// /// Encoder for writing the data image to a stream in jpeg format. /// -public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions +public sealed class JpegEncoder : ImageEncoder { /// /// Backing field for . /// private int? quality; - /// + /// + /// Gets the quality, that will be used to encode the image. Quality + /// index must be between 1 and 100 (compression from max to min). + /// Defaults to 75. + /// + /// Quality factor must be in [1..100] range. public int? Quality { get => this.quality; - set + init { if (value is < 1 or > 100) { @@ -30,37 +33,31 @@ public int? Quality } } - /// - public bool? Interleaved { get; set; } - - /// - public JpegEncodingColor? ColorType { get; set; } + /// + /// Gets the component encoding mode. + /// + /// + /// Interleaved encoding mode encodes all color components in a single scan. + /// Non-interleaved encoding mode encodes each color component in a separate scan. + /// + public bool? Interleaved { get; init; } /// - /// Encodes the image to the specified stream from the . + /// Gets the jpeg color for encoding. /// - /// 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 JpegEncodingColor? ColorType { get; init; } + + /// + public override void Encode(Image image, Stream stream) { - var encoder = new JpegEncoderCore(this); + JpegEncoderCore encoder = new(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 Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new JpegEncoderCore(this); + JpegEncoderCore encoder = new(this); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 83c2e27e91..e915b74bc3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -29,7 +29,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// private readonly byte[] buffer = new byte[20]; - private readonly IJpegEncoderOptions options; + private readonly JpegEncoder encoder; /// /// The output stream. All attempted writes after the first error become no-ops. @@ -39,9 +39,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// /// Initializes a new instance of the class. /// - /// The options. - public JpegEncoderCore(IJpegEncoderOptions options) - => this.options = options; + /// The parent encoder. + public JpegEncoderCore(JpegEncoder encoder) + => this.encoder = encoder; public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4]; @@ -71,8 +71,8 @@ public void Encode(Image image, Stream stream, CancellationToken JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata); - bool interleaved = this.options.Interleaved ?? jpegMetadata.Interleaved ?? true; - using var frame = new JpegFrame(image, frameConfig, interleaved); + bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved ?? true; + using JpegFrame frame = new(image, frameConfig, interleaved); // Write the Start Of Image marker. this.WriteStartOfImage(); @@ -96,14 +96,14 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteStartOfFrame(image.Width, image.Height, frameConfig); // Write the Huffman tables. - var scanEncoder = new HuffmanScanEncoder(frame.BlocksPerMcu, stream); + HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder); // Write the quantization tables. - this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.options.Quality, jpegMetadata); + this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata); // Write scans with actual pixel data - using var spectralConverter = new SpectralConverter(frame, image, this.QuantizationTables); + using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables); this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken); // Write the End Of Image marker. @@ -172,6 +172,9 @@ private void WriteJfifApplicationHeader(ImageMetadata meta) /// /// Writes the Define Huffman Table marker and tables. /// + /// The table configuration. + /// The scan encoder. + /// is . private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder) { if (tableConfigs is null) @@ -203,6 +206,7 @@ private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, Huf /// /// Writes the APP14 marker to indicate the image is in RGB color space. /// + /// The color transform byte. private void WriteApp14Marker(byte colorTransform) { this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length); @@ -498,6 +502,9 @@ private void WriteProfiles(ImageMetadata metadata) /// /// Writes the Start Of Frame (Baseline) marker. /// + /// The frame width. + /// The frame height. + /// The frame configuration. private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) { JpegComponentConfig[] components = frame.Components; @@ -536,6 +543,7 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) /// /// Writes the StartOfScan marker. /// + /// The collecction of component configuration items. private void WriteStartOfScan(Span components) { // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: @@ -588,7 +596,18 @@ private void WriteEndOfImageMarker() /// /// Writes scans for given config. /// - private void WriteHuffmanScans(JpegFrame frame, JpegFrameConfig frameConfig, SpectralConverter spectralConverter, HuffmanScanEncoder encoder, CancellationToken cancellationToken) + /// The type of pixel format. + /// The current frame. + /// The frame configuration. + /// The spectral converter. + /// The scan encoder. + /// The cancellation token. + private void WriteHuffmanScans( + JpegFrame frame, + JpegFrameConfig frameConfig, + SpectralConverter spectralConverter, + HuffmanScanEncoder encoder, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (frame.Components.Length == 1) @@ -696,7 +715,7 @@ private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs private JpegFrameConfig GetFrameConfig(JpegMetadata metadata) { - JpegEncodingColor color = this.options.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420; + JpegEncodingColor color = this.encoder.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420; JpegFrameConfig frameConfig = Array.Find( FrameConfigs, cfg => cfg.EncodingColor == color); diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs index e0e93cc11e..6b25347c04 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm; @@ -30,36 +29,34 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// The specification of these images is found at . /// -public sealed class PbmEncoder : IImageEncoder, IPbmEncoderOptions +public sealed class PbmEncoder : ImageEncoder { /// - /// Gets or sets the Encoding of the pixels. + /// Gets the encoding of the pixels. /// - public PbmEncoding? Encoding { get; set; } + public PbmEncoding? Encoding { get; init; } /// - /// Gets or sets the Color type of the resulting image. + /// Gets the Color type of the resulting image. /// - public PbmColorType? ColorType { get; set; } + public PbmColorType? ColorType { get; init; } /// - /// Gets or sets the data type of the pixels components. + /// Gets the Data Type of the pixel components. /// - public PbmComponentType? ComponentType { get; set; } + public PbmComponentType? ComponentType { get; init; } /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { - var encoder = new PbmEncoderCore(image.GetConfiguration(), this); + PbmEncoderCore encoder = new(image.GetConfiguration(), this); encoder.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new PbmEncoderCore(image.GetConfiguration(), this); + PbmEncoderCore encoder = new(image.GetConfiguration(), this); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index c7d8d183c2..7c7860c58e 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -22,9 +22,9 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals private Configuration configuration; /// - /// The encoder options. + /// The encoder with options. /// - private readonly IPbmEncoderOptions options; + private readonly PbmEncoder encoder; /// /// The encoding for the pixels. @@ -45,11 +45,11 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals /// Initializes a new instance of the class. /// /// The configuration. - /// The encoder options. - public PbmEncoderCore(Configuration configuration, IPbmEncoderOptions options) + /// The encoder with options. + public PbmEncoderCore(Configuration configuration, PbmEncoder encoder) { this.configuration = configuration; - this.options = options; + this.encoder = encoder; } /// @@ -65,7 +65,7 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.DeduceOptions(image); + this.SanitizeAndSetEncoderOptions(image); byte signature = this.DeduceSignature(); this.WriteHeader(stream, signature, image.Size()); @@ -75,16 +75,16 @@ public void Encode(Image image, Stream stream, CancellationToken stream.Flush(); } - private void DeduceOptions(Image image) + private void SanitizeAndSetEncoderOptions(Image image) where TPixel : unmanaged, IPixel { this.configuration = image.GetConfiguration(); PbmMetadata metadata = image.Metadata.GetPbmMetadata(); - this.encoding = this.options.Encoding ?? metadata.Encoding; - this.colorType = this.options.ColorType ?? metadata.ColorType; + this.encoding = this.encoder.Encoding ?? metadata.Encoding; + this.colorType = this.encoder.ColorType ?? metadata.ColorType; if (this.colorType != PbmColorType.BlackAndWhite) { - this.componentType = this.options.ComponentType ?? metadata.ComponentType; + this.componentType = this.encoder.ComponentType ?? metadata.ComponentType; } else { 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..610ea69240 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -5,6 +5,7 @@ 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.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 @@ public void Encode(Image image, Stream stream, CancellationToken ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); - PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); + this.SanitizeAndSetEncoderOptions(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 @@ private static void ClearTransparentPixels(Image image) 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 @@ private static void ClearTransparentPixels(Image image) }); /// - /// 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 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) 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 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan) 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 @@ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImag /// 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 @@ private void ApplyOptimalFilteredScanline(ref Span filter, ref Span { // 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 @@ private void WriteHeaderChunk(Stream stream) 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 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame 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 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame /// 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 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta) /// 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 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) 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 @@ private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData) /// 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 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) { // 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 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) 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 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) 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 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) 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 @@ private byte[] GetZlibCompressedBytes(byte[] dataBytes) /// 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 @@ private void WriteDataChunks(Image pixels, IndexedImageFrame 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 SanitizeAndSetEncoderOptions( + 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( + QuantizingImageEncoder 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.PixelSamplingStrategy, 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) < 0) + { + 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/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs b/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs deleted file mode 100644 index a42feb7f35..0000000000 --- a/src/ImageSharp/Formats/Tga/ITgaEncoderOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Tga; - -/// -/// Configuration options for use during tga encoding. -/// -internal interface ITgaEncoderOptions -{ - /// - /// Gets the number of bits per pixel. - /// - TgaBitsPerPixel? BitsPerPixel { get; } - - /// - /// Gets a value indicating whether run length compression should be used. - /// - TgaCompression Compression { get; } -} diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index f437e80a3f..e0a3932355 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -2,38 +2,35 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tga; /// /// Image encoder for writing an image to a stream as a targa truevision image. /// -public sealed class TgaEncoder : IImageEncoder, ITgaEncoderOptions +public sealed class TgaEncoder : ImageEncoder { /// - /// Gets or sets the number of bits per pixel. + /// Gets the number of bits per pixel. /// - public TgaBitsPerPixel? BitsPerPixel { get; set; } + public TgaBitsPerPixel? BitsPerPixel { get; init; } /// - /// Gets or sets a value indicating whether no compression or run length compression should be used. + /// Gets a value indicating whether no compression or run length compression should be used. /// - public TgaCompression Compression { get; set; } = TgaCompression.RunLength; + public TgaCompression Compression { get; init; } = TgaCompression.RunLength; /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { - var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); + TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); encoder.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); + TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 016806db03..0a3eb74aed 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -45,13 +45,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals /// /// Initializes a new instance of the class. /// - /// The encoder options. + /// The encoder with options. /// The memory manager. - public TgaEncoderCore(ITgaEncoderOptions options, MemoryAllocator memoryAllocator) + public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; - this.bitsPerPixel = options.BitsPerPixel; - this.compression = options.Compression; + this.bitsPerPixel = encoder.BitsPerPixel; + this.compression = encoder.Compression; } /// @@ -105,7 +105,9 @@ public void Encode(Image image, Stream stream, CancellationToken cMapLength: 0, cMapDepth: 0, xOffset: 0, - yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, // When run length encoding is used, the origin should be top left instead of the default bottom left. + + // When run length encoding is used, the origin should be top left instead of the default bottom left. + yOffset: this.compression is TgaCompression.RunLength ? (short)image.Height : (short)0, width: (short)image.Width, height: (short)image.Height, pixelDepth: (byte)this.bitsPerPixel.Value, diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs deleted file mode 100644 index 1e74e630ce..0000000000 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Tiff.Constants; -using SixLabors.ImageSharp.Processing.Processors.Quantization; - -namespace SixLabors.ImageSharp.Formats.Tiff; - -/// -/// Encapsulates the options for the . -/// -internal interface ITiffEncoderOptions -{ - /// - /// Gets the number of bits per pixel. - /// - TiffBitsPerPixel? BitsPerPixel { get; } - - /// - /// Gets the compression type to use. - /// - TiffCompression? Compression { get; } - - /// - /// Gets the compression level 1-9 for the deflate compression mode. - /// Defaults to . - /// - DeflateCompressionLevel? CompressionLevel { get; } - - /// - /// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor. - /// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used. - /// - TiffPhotometricInterpretation? PhotometricInterpretation { get; } - - /// - /// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression. - /// - TiffPredictor? HorizontalPredictor { get; } - - /// - /// Gets the quantizer for creating a color palette image. - /// - IQuantizer Quantizer { get; } -} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 3b5d347722..e7bb08cdc3 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -4,47 +4,52 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Constants; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Encoder for writing the data image to a stream in TIFF format. /// -public class TiffEncoder : IImageEncoder, ITiffEncoderOptions +public class TiffEncoder : QuantizingImageEncoder { - /// - public TiffBitsPerPixel? BitsPerPixel { get; set; } - - /// - public TiffCompression? Compression { get; set; } - - /// - public DeflateCompressionLevel? CompressionLevel { get; set; } - - /// - public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } - - /// - public TiffPredictor? HorizontalPredictor { get; set; } - - /// - public IQuantizer Quantizer { get; set; } - - /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + /// + /// Gets the number of bits per pixel. + /// + public TiffBitsPerPixel? BitsPerPixel { get; init; } + + /// + /// Gets the compression type to use. + /// + public TiffCompression? Compression { get; init; } + + /// + /// Gets the compression level 1-9 for the deflate compression mode. + /// Defaults to . + /// + public DeflateCompressionLevel? CompressionLevel { get; init; } + + /// + /// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor. + /// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used. + /// + public TiffPhotometricInterpretation? PhotometricInterpretation { get; init; } + + /// + /// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression. + /// + public TiffPredictor? HorizontalPredictor { get; init; } + + /// + public override void Encode(Image image, Stream stream) { - var encode = new TiffEncoderCore(this, image.GetMemoryAllocator()); + TiffEncoderCore encode = new(this, image.GetMemoryAllocator()); encode.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new TiffEncoderCore(this, image.GetMemoryAllocator()); + TiffEncoderCore encoder = new(this, image.GetMemoryAllocator()); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 9b50b958c8..2013377ed6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -40,10 +39,15 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals private Configuration configuration; /// - /// The quantizer for creating color palette image. + /// The quantizer for creating color palette images. /// private readonly IQuantizer quantizer; + /// + /// The pixel sampling strategy for quantization. + /// + private readonly IPixelSamplingStrategy pixelSamplingStrategy; + /// /// Sets the deflate compression level. /// @@ -69,6 +73,11 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals /// private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + private readonly List<(long, uint)> frameMarkers = new(); /// @@ -76,15 +85,17 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals /// /// The options for the encoder. /// The memory allocator. - public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator) + public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; this.PhotometricInterpretation = options.PhotometricInterpretation; - this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = options.Quantizer; + this.pixelSamplingStrategy = options.PixelSamplingStrategy; this.BitsPerPixel = options.BitsPerPixel; this.HorizontalPredictor = options.HorizontalPredictor; this.CompressionType = options.Compression; this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; + this.skipMetadata = options.SkipMetadata; } /// @@ -215,6 +226,7 @@ private long WriteFrame( this.PhotometricInterpretation, frame, this.quantizer, + this.pixelSamplingStrategy, this.memoryAllocator, this.configuration, entriesCollector, @@ -226,7 +238,7 @@ private long WriteFrame( if (image != null) { - entriesCollector.ProcessMetadata(image); + entriesCollector.ProcessMetadata(image, this.skipMetadata); } entriesCollector.ProcessFrameInfo(frame, imageMetadata); @@ -331,7 +343,12 @@ private long WriteIfd(TiffStreamWriter writer, List entries) return nextIfdMarker; } - private void SanitizeAndSetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, int inputBitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) + private void SanitizeAndSetEncoderOptions( + TiffBitsPerPixel? bitsPerPixel, + int inputBitsPerPixel, + TiffPhotometricInterpretation? photometricInterpretation, + TiffCompression compression, + TiffPredictor predictor) { // BitsPerPixel should be the primary source of truth for the encoder options. if (bitsPerPixel.HasValue) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index e158900cdc..64b300cd60 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -15,8 +15,8 @@ internal class TiffEncoderEntriesCollector public List Entries { get; } = new List(); - public void ProcessMetadata(Image image) - => new MetadataProcessor(this).Process(image); + public void ProcessMetadata(Image image, bool skipMetadata) + => new MetadataProcessor(this).Process(image, skipMetadata); public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) => new FrameInfoProcessor(this).Process(frame, imageMetadata); @@ -41,7 +41,7 @@ public void AddOrReplace(IExifValue entry) private abstract class BaseProcessor { - public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; + protected BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; protected TiffEncoderEntriesCollector Collector { get; } } @@ -53,14 +53,18 @@ public MetadataProcessor(TiffEncoderEntriesCollector collector) { } - public void Process(Image image) + public void Process(Image image, bool skipMetadata) { ImageFrame rootFrame = image.Frames.RootFrame; - ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); + ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile; XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile); - this.ProcessMetadata(rootFrameExifProfile); + this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile); + + if (!skipMetadata) + { + this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile()); + } if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) { @@ -72,39 +76,35 @@ public void Process(Image image) } private static bool IsPureMetadata(ExifTag tag) - { - switch ((ExifTagValue)(ushort)tag) + => (ExifTagValue)(ushort)tag switch { - case ExifTagValue.DocumentName: - case ExifTagValue.ImageDescription: - case ExifTagValue.Make: - case ExifTagValue.Model: - case ExifTagValue.Software: - case ExifTagValue.DateTime: - case ExifTagValue.Artist: - case ExifTagValue.HostComputer: - case ExifTagValue.TargetPrinter: - case ExifTagValue.XMP: - case ExifTagValue.Rating: - case ExifTagValue.RatingPercent: - case ExifTagValue.ImageID: - case ExifTagValue.Copyright: - case ExifTagValue.MDLabName: - case ExifTagValue.MDSampleInfo: - case ExifTagValue.MDPrepDate: - case ExifTagValue.MDPrepTime: - case ExifTagValue.MDFileUnits: - case ExifTagValue.SEMInfo: - case ExifTagValue.XPTitle: - case ExifTagValue.XPComment: - case ExifTagValue.XPAuthor: - case ExifTagValue.XPKeywords: - case ExifTagValue.XPSubject: - return true; - default: - return false; - } - } + ExifTagValue.DocumentName or + ExifTagValue.ImageDescription or + ExifTagValue.Make or + ExifTagValue.Model or + ExifTagValue.Software or + ExifTagValue.DateTime or + ExifTagValue.Artist or + ExifTagValue.HostComputer or + ExifTagValue.TargetPrinter or + ExifTagValue.XMP or + ExifTagValue.Rating or + ExifTagValue.RatingPercent or + ExifTagValue.ImageID or + ExifTagValue.Copyright or + ExifTagValue.MDLabName or + ExifTagValue.MDSampleInfo or + ExifTagValue.MDPrepDate or + ExifTagValue.MDPrepTime or + ExifTagValue.MDFileUnits or + ExifTagValue.SEMInfo or + ExifTagValue.XPTitle or + ExifTagValue.XPComment or + ExifTagValue.XPAuthor or + ExifTagValue.XPKeywords or + ExifTagValue.XPSubject => true, + _ => false, + }; private void ProcessMetadata(ExifProfile exifProfile) { @@ -149,9 +149,9 @@ private void ProcessMetadata(ExifProfile exifProfile) } } - private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) + private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) { - if (exifProfile != null && exifProfile.Parts != ExifParts.None) + if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None)) { foreach (IExifValue entry in exifProfile.Values) { @@ -167,13 +167,13 @@ private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfil } else { - exifProfile.RemoveValue(ExifTag.SubIFDOffset); + exifProfile?.RemoveValue(ExifTag.SubIFDOffset); } - if (imageMetadata.IptcProfile != null) + if (!skipMetadata && imageMetadata.IptcProfile != null) { imageMetadata.IptcProfile.UpdateData(); - var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte) + ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte) { Value = imageMetadata.IptcProfile.Data }; @@ -182,12 +182,12 @@ private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfil } else { - exifProfile.RemoveValue(ExifTag.IPTC); + exifProfile?.RemoveValue(ExifTag.IPTC); } if (imageMetadata.IccProfile != null) { - var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined) + ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined) { Value = imageMetadata.IccProfile.ToByteArray() }; @@ -196,12 +196,12 @@ private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfil } else { - exifProfile.RemoveValue(ExifTag.IccProfile); + exifProfile?.RemoveValue(ExifTag.IccProfile); } - if (xmpProfile != null) + if (!skipMetadata && xmpProfile != null) { - var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) + ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) { Value = xmpProfile.Data }; @@ -210,7 +210,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfil } else { - exifProfile.RemoveValue(ExifTag.XMP); + exifProfile?.RemoveValue(ExifTag.XMP); } } } @@ -273,29 +273,29 @@ public ImageFormatProcessor(TiffEncoderEntriesCollector collector) public void Process(TiffEncoderCore encoder) { - var planarConfig = new ExifShort(ExifTagValue.PlanarConfiguration) + ExifShort planarConfig = new(ExifTagValue.PlanarConfiguration) { Value = (ushort)TiffPlanarConfiguration.Chunky }; - var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) + ExifLong samplesPerPixel = new(ExifTagValue.SamplesPerPixel) { Value = GetSamplesPerPixel(encoder) }; ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder); - var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) + ExifShortArray bitPerSample = new(ExifTagValue.BitsPerSample) { Value = bitsPerSampleValue }; ushort compressionType = GetCompressionType(encoder); - var compression = new ExifShort(ExifTagValue.Compression) + ExifShort compression = new(ExifTagValue.Compression) { Value = compressionType }; - var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) + ExifShort photometricInterpretation = new(ExifTagValue.PhotometricInterpretation) { Value = (ushort)encoder.PhotometricInterpretation }; @@ -306,32 +306,25 @@ public void Process(TiffEncoderCore encoder) this.Collector.AddOrReplace(compression); this.Collector.AddOrReplace(photometricInterpretation); - if (encoder.HorizontalPredictor == TiffPredictor.Horizontal) + if (encoder.HorizontalPredictor == TiffPredictor.Horizontal && + (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero)) { - if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb || - encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor || - encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) - { - var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; + ExifShort predictor = new(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; - this.Collector.AddOrReplace(predictor); - } + this.Collector.AddOrReplace(predictor); } } private static uint GetSamplesPerPixel(TiffEncoderCore encoder) - { - switch (encoder.PhotometricInterpretation) + => encoder.PhotometricInterpretation switch { - case TiffPhotometricInterpretation.PaletteColor: - case TiffPhotometricInterpretation.BlackIsZero: - case TiffPhotometricInterpretation.WhiteIsZero: - return 1; - case TiffPhotometricInterpretation.Rgb: - default: - return 3; - } - } + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero or + TiffPhotometricInterpretation.WhiteIsZero => 1, + _ => 3, + }; private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) { @@ -342,10 +335,8 @@ private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) { return TiffConstants.BitsPerSample4Bit.ToArray(); } - else - { - return TiffConstants.BitsPerSample8Bit.ToArray(); - } + + return TiffConstants.BitsPerSample8Bit.ToArray(); case TiffPhotometricInterpretation.Rgb: return TiffConstants.BitsPerSampleRgb8Bit.ToArray(); @@ -382,9 +373,9 @@ private static ushort GetCompressionType(TiffEncoderCore encoder) // PackBits is allowed for all modes. return (ushort)TiffCompression.PackBits; case TiffCompression.Lzw: - if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb || - encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor || - encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) + if (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or + TiffPhotometricInterpretation.PaletteColor or + TiffPhotometricInterpretation.BlackIsZero) { return (ushort)TiffCompression.Lzw; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs index bb13137cef..a52d49a353 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -14,6 +14,7 @@ public static TiffBaseColorWriter Create( TiffPhotometricInterpretation? photometricInterpretation, ImageFrame image, IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector, @@ -23,7 +24,7 @@ public static TiffBaseColorWriter Create( switch (photometricInterpretation) { case TiffPhotometricInterpretation.PaletteColor: - return new TiffPaletteWriter(image, quantizer, memoryAllocator, configuration, entriesCollector, bitsPerPixel); + return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); case TiffPhotometricInterpretation.BlackIsZero: case TiffPhotometricInterpretation.WhiteIsZero: if (bitsPerPixel == 1) diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs index f8810d65ac..87118d6f76 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -17,19 +17,21 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter private readonly int maxColors; private readonly int colorPaletteSize; private readonly int colorPaletteBytes; - private readonly IndexedImageFrame quantizedImage; + private readonly IndexedImageFrame quantizedFrame; private IMemoryOwner indexedPixelsBuffer; public TiffPaletteWriter( - ImageFrame image, + ImageFrame frame, IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) - : base(image, memoryAllocator, configuration, entriesCollector) + : base(frame, memoryAllocator, configuration, entriesCollector) { DebugGuard.NotNull(quantizer, nameof(quantizer)); + DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); DebugGuard.NotNull(configuration, nameof(configuration)); DebugGuard.NotNull(entriesCollector, nameof(entriesCollector)); DebugGuard.MustBeBetweenOrEqualTo(bitsPerPixel, 4, 8, nameof(bitsPerPixel)); @@ -38,11 +40,15 @@ public TiffPaletteWriter( this.maxColors = this.BitsPerPixel == 4 ? 16 : 256; this.colorPaletteSize = this.maxColors * 3; this.colorPaletteBytes = this.colorPaletteSize * 2; - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration, new QuantizerOptions() - { - MaxColors = this.maxColors - }); - this.quantizedImage = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer( + this.Configuration, + new QuantizerOptions() + { + MaxColors = this.maxColors + }); + + frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); + this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.AddColorMapTag(); } @@ -66,7 +72,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int lastRow = y + height; for (int row = y; row < lastRow; row++) { - ReadOnlySpan indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row); + ReadOnlySpan indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row); int idxPixels = 0; for (int x = 0; x < halfWidth; x++) { @@ -93,7 +99,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int indexedPixelsRowIdx = 0; for (int row = y; row < lastRow; row++) { - ReadOnlySpan indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row); + ReadOnlySpan indexedPixelRow = this.quantizedFrame.DangerousGetRowSpan(row); indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width)); indexedPixelsRowIdx++; } @@ -105,7 +111,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre /// protected override void Dispose(bool disposing) { - this.quantizedImage?.Dispose(); + this.quantizedFrame?.Dispose(); this.indexedPixelsBuffer?.Dispose(); } @@ -114,7 +120,7 @@ private void AddColorMapTag() using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.Allocate(this.colorPaletteBytes); Span colorPalette = colorPaletteBuffer.GetSpan(); - ReadOnlySpan quantizedColors = this.quantizedImage.Palette.Span; + ReadOnlySpan quantizedColors = this.quantizedFrame.Palette.Span; int quantizedColorBytes = quantizedColors.Length * 3 * 2; // In the ColorMap, black is represented by 0, 0, 0 and white is represented by 65535, 65535, 65535. @@ -126,7 +132,7 @@ private void AddColorMapTag() // In a TIFF ColorMap, all the Red values come first, followed by the Green values, // then the Blue values. Convert the quantized palette to this format. - var palette = new ushort[this.colorPaletteSize]; + ushort[] palette = new ushort[this.colorPaletteSize]; int paletteIdx = 0; for (int i = 0; i < quantizedColors.Length; i++) { @@ -147,7 +153,7 @@ private void AddColorMapTag() palette[paletteIdx++] = quantizedColorRgb48[i].B; } - var colorMap = new ExifShortArray(ExifTagValue.ColorMap) + ExifShortArray colorMap = new(ExifTagValue.ColorMap) { Value = palette }; diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 1e60235e26..fc5580a4a7 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -24,10 +24,11 @@ internal class AlphaEncoder : IDisposable /// The to encode from. /// The global configuration. /// The memory manager. + /// Whether to skip metadata encoding. /// Indicates, if the data should be compressed with the lossless webp compression. /// The size in bytes of the alpha data. /// The encoded alpha data. - public IMemoryOwner EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size) + public IMemoryOwner EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, bool compress, out int size) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -36,14 +37,15 @@ public IMemoryOwner EncodeAlpha(Image image, Configuration if (compress) { - WebpEncodingMethod effort = WebpEncodingMethod.Default; - int quality = 8 * (int)effort; - using var lossLessEncoder = new Vp8LEncoder( + const WebpEncodingMethod effort = WebpEncodingMethod.Default; + const int quality = 8 * (int)effort; + using Vp8LEncoder lossLessEncoder = new( memoryAllocator, configuration, width, height, quality, + skipMetadata, effort, WebpTransparentColorMode.Preserve, false, @@ -75,7 +77,7 @@ private static Image DispatchAlphaToGreen(Image image, S { int width = image.Width; int height = image.Height; - var alphaAsImage = new Image(width, height); + Image alphaAsImage = new(width, height); for (int y = 0; y < height; y++) { diff --git a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs deleted file mode 100644 index bc316d08c7..0000000000 --- a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Configuration options for use during webp encoding. -/// -internal interface IWebpEncoderOptions -{ - /// - /// Gets the webp file format used. Either lossless or lossy. - /// Defaults to lossy. - /// - WebpFileFormatType? FileFormat { get; } - - /// - /// Gets the compression quality. Between 0 and 100. - /// For lossy, 0 gives the smallest size and 100 the largest. For lossless, - /// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger - /// files compared to the slowest, but best, 100. - /// Defaults to 75. - /// - int Quality { get; } - - /// - /// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better). - /// Defaults to 4. - /// - WebpEncodingMethod Method { get; } - - /// - /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. - /// Defaults to true. - /// - bool UseAlphaCompression { get; } - - /// - /// Gets the number of entropy-analysis passes (in [1..10]). - /// Defaults to 1. - /// - int EntropyPasses { get; } - - /// - /// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms - /// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits. - /// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect). - /// Defaults to 50. - /// - int SpatialNoiseShaping { get; } - - /// - /// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). - /// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture. - /// The higher the value the smoother the picture will appear. - /// Typical values are usually in the range of 20 to 50. - /// Defaults to 60. - /// - int FilterStrength { get; } - - /// - /// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible - /// RGB information for better compression. - /// The default value is Clear. - /// - WebpTransparentColorMode TransparentColorMode { get; } - - /// - /// Gets a value indicating whether near lossless mode should be used. - /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality. - /// - bool NearLossless { get; } - - /// - /// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). - /// The typical value is around 60. Note that lossy with -q 100 can at times yield better results. - /// - int NearLosslessQuality { get; } -} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 3917c863b9..7f1f4f4e2f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -8,6 +8,8 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; @@ -67,6 +69,11 @@ internal class Vp8LEncoder : IDisposable /// private readonly WebpTransparentColorMode transparentColorMode; + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + /// /// Indicating whether near lossless mode should be used. /// @@ -91,6 +98,7 @@ internal class Vp8LEncoder : IDisposable /// The width of the input image. /// The height of the input image. /// The encoding quality. + /// Whether to skip metadata encoding. /// Quality/speed trade-off (0=fast, 6=slower-better). /// Flag indicating whether to preserve the exact RGB values under transparent area. /// Otherwise, discard this invisible RGB information for better compression. @@ -102,6 +110,7 @@ public Vp8LEncoder( int width, int height, int quality, + bool skipMetadata, WebpEncodingMethod method, WebpTransparentColorMode transparentColorMode, bool nearLossless, @@ -113,6 +122,7 @@ public Vp8LEncoder( this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.quality = Numerics.Clamp(quality, 0, 100); + this.skipMetadata = skipMetadata; this.method = method; this.transparentColorMode = transparentColorMode; this.nearLossless = nearLossless; @@ -239,6 +249,9 @@ public void Encode(Image image, Stream stream) ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + // Convert image pixels to bgra array. bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); @@ -252,7 +265,7 @@ public void Encode(Image image, Stream stream) this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index b15ccc052b..309e4175a0 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -6,6 +6,8 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossy; @@ -55,6 +57,11 @@ internal class Vp8Encoder : IDisposable /// private Vp8BitWriter bitWriter; + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + private readonly Vp8RdLevel rdOptLevel; private int maxI4HeaderBits; @@ -94,6 +101,7 @@ internal class Vp8Encoder : IDisposable /// The width of the input image. /// The height of the input image. /// The encoding quality. + /// Whether to skip metadata encoding. /// Quality/speed trade-off (0=fast, 6=slower-better). /// Number of entropy-analysis passes (in [1..10]). /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). @@ -105,6 +113,7 @@ public Vp8Encoder( int width, int height, int quality, + bool skipMetadata, WebpEncodingMethod method, int entropyPasses, int filterStrength, @@ -116,6 +125,7 @@ public Vp8Encoder( this.Width = width; this.Height = height; this.quality = Numerics.Clamp(quality, 0, 100); + this.skipMetadata = skipMetadata; this.method = method; this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); @@ -342,7 +352,7 @@ public void Encode(Image image, Stream stream) if (hasAlpha) { // TODO: This can potentially run in an separate task. - IMemoryOwner encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize); + IMemoryOwner encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.skipMetadata, this.alphaCompression, out alphaDataSize); alphaData = encodedAlphaData.GetSpan(); if (alphaDataSize < pixelCount) { @@ -384,10 +394,14 @@ public void Encode(Image image, Stream stream) // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + this.bitWriter.WriteEncodedImageToStream( stream, - metadata.ExifProfile, - metadata.XmpProfile, + exifProfile, + xmpProfile, metadata.IccProfile, (uint)width, (uint)height, diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index b6a45555d8..359128254f 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -2,58 +2,94 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image encoder for writing an image to a stream in the Webp format. /// -public sealed class WebpEncoder : IImageEncoder, IWebpEncoderOptions +public sealed class WebpEncoder : ImageEncoder { - /// - public WebpFileFormatType? FileFormat { get; set; } + /// + /// Gets the webp file format used. Either lossless or lossy. + /// Defaults to lossy. + /// + public WebpFileFormatType? FileFormat { get; init; } - /// - public int Quality { get; set; } = 75; + /// + /// Gets the compression quality. Between 0 and 100. + /// For lossy, 0 gives the smallest size and 100 the largest. For lossless, + /// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger + /// files compared to the slowest, but best, 100. + /// Defaults to 75. + /// + public int Quality { get; init; } = 75; - /// - public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default; + /// + /// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better). + /// Defaults to 4. + /// + public WebpEncodingMethod Method { get; init; } = WebpEncodingMethod.Default; - /// - public bool UseAlphaCompression { get; set; } = true; + /// + /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. + /// Defaults to true. + /// + public bool UseAlphaCompression { get; init; } = true; - /// - public int EntropyPasses { get; set; } = 1; + /// + /// Gets the number of entropy-analysis passes (in [1..10]). + /// Defaults to 1. + /// + public int EntropyPasses { get; init; } = 1; - /// - public int SpatialNoiseShaping { get; set; } = 50; + /// + /// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms + /// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits. + /// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect). + /// Defaults to 50. + /// + public int SpatialNoiseShaping { get; init; } = 50; - /// - public int FilterStrength { get; set; } = 60; + /// + /// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture. + /// The higher the value the smoother the picture will appear. + /// Typical values are usually in the range of 20 to 50. + /// Defaults to 60. + /// + public int FilterStrength { get; init; } = 60; - /// - public WebpTransparentColorMode TransparentColorMode { get; set; } = WebpTransparentColorMode.Clear; + /// + /// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// The default value is Clear. + /// + public WebpTransparentColorMode TransparentColorMode { get; init; } = WebpTransparentColorMode.Clear; - /// - public bool NearLossless { get; set; } + /// + /// Gets a value indicating whether near lossless mode should be used. + /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality. + /// + public bool NearLossless { get; init; } - /// - public int NearLosslessQuality { get; set; } = 100; + /// + /// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// The typical value is around 60. Note that lossy with -q 100 can at times yield better results. + /// + public int NearLosslessQuality { get; init; } = 100; /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + public override void Encode(Image image, Stream stream) { - var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); + WebpEncoderCore encoder = new(this, image.GetMemoryAllocator()); encoder.Encode(image, stream); } /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public override Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) { - var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); + WebpEncoderCore encoder = new(this, image.GetMemoryAllocator()); return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index f9ceaf3098..e8ee316d88 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -56,6 +56,11 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals /// private readonly WebpTransparentColorMode transparentColorMode; + /// + /// Whether to skip metadata during encoding. + /// + private readonly bool skipMetadata; + /// /// Indicating whether near lossless mode should be used. /// @@ -80,21 +85,22 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals /// /// Initializes a new instance of the class. /// - /// The encoder options. + /// The encoder with options. /// The memory manager. - public WebpEncoderCore(IWebpEncoderOptions options, MemoryAllocator memoryAllocator) + public WebpEncoderCore(WebpEncoder encoder, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; - this.alphaCompression = options.UseAlphaCompression; - this.fileFormat = options.FileFormat; - this.quality = options.Quality; - this.method = options.Method; - this.entropyPasses = options.EntropyPasses; - this.spatialNoiseShaping = options.SpatialNoiseShaping; - this.filterStrength = options.FilterStrength; - this.transparentColorMode = options.TransparentColorMode; - this.nearLossless = options.NearLossless; - this.nearLosslessQuality = options.NearLosslessQuality; + this.alphaCompression = encoder.UseAlphaCompression; + this.fileFormat = encoder.FileFormat; + this.quality = encoder.Quality; + this.method = encoder.Method; + this.entropyPasses = encoder.EntropyPasses; + this.spatialNoiseShaping = encoder.SpatialNoiseShaping; + this.filterStrength = encoder.FilterStrength; + this.transparentColorMode = encoder.TransparentColorMode; + this.skipMetadata = encoder.SkipMetadata; + this.nearLossless = encoder.NearLossless; + this.nearLosslessQuality = encoder.NearLosslessQuality; } /// @@ -124,12 +130,13 @@ public void Encode(Image image, Stream stream, CancellationToken if (lossless) { - using var enc = new Vp8LEncoder( + using Vp8LEncoder enc = new( this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, + this.skipMetadata, this.method, this.transparentColorMode, this.nearLossless, @@ -138,12 +145,13 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - using var enc = new Vp8Encoder( + using Vp8Encoder enc = new( this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, + this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, diff --git a/src/ImageSharp/ImageExtensions.Internal.cs b/src/ImageSharp/ImageExtensions.Internal.cs index 76c9b85224..6ec95a564a 100644 --- a/src/ImageSharp/ImageExtensions.Internal.cs +++ b/src/ImageSharp/ImageExtensions.Internal.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Memory; @@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp; public static partial class ImageExtensions { /// - /// Locks the image providing access to the pixels. + /// Provides access to the image pixels. /// - /// It is imperative that the accessor is correctly disposed off after use. + /// It is imperative that the accessor is correctly disposed of after use. /// /// /// The type of the pixel. @@ -24,7 +24,5 @@ public static partial class ImageExtensions /// internal static Buffer2D GetRootFramePixelBuffer(this Image image) where TPixel : unmanaged, IPixel - { - return image.Frames.RootFrame.PixelBuffer; - } + => image.Frames.RootFrame.PixelBuffer; } diff --git a/src/ImageSharp/ImageFrameCollection.cs b/src/ImageSharp/ImageFrameCollection.cs index 1632134260..cc2b430ff1 100644 --- a/src/ImageSharp/ImageFrameCollection.cs +++ b/src/ImageSharp/ImageFrameCollection.cs @@ -180,7 +180,7 @@ public void Dispose() } /// - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() { this.EnsureNotDisposed(); @@ -188,7 +188,7 @@ public IEnumerator GetEnumerator() } /// - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); /// /// Throws if the image frame is disposed. @@ -208,7 +208,7 @@ protected void EnsureNotDisposed() protected abstract void Dispose(bool disposing); /// - /// Implements . + /// Implements . /// /// The enumerator. protected abstract IEnumerator NonGenericGetEnumerator(); diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index 60b5c6d6ad..faa83b59e2 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -168,7 +168,7 @@ public ImageFrame AddFrame(ReadOnlySpan source) { this.EnsureNotDisposed(); - var frame = ImageFrame.LoadPixelData( + ImageFrame frame = ImageFrame.LoadPixelData( this.parent.GetConfiguration(), source, this.RootFrame.Width, @@ -298,7 +298,7 @@ public override void MoveFrame(int sourceIndex, int destinationIndex) { this.EnsureNotDisposed(); - var frame = new ImageFrame( + ImageFrame frame = new( this.parent.GetConfiguration(), this.RootFrame.Width, this.RootFrame.Height); @@ -364,7 +364,7 @@ protected override ImageFrame NonGenericCreateFrame(Color backgroundColor) => /// public ImageFrame CreateFrame(TPixel backgroundColor) { - var frame = new ImageFrame( + ImageFrame frame = new( this.parent.GetConfiguration(), this.RootFrame.Width, this.RootFrame.Height, @@ -374,10 +374,15 @@ public ImageFrame CreateFrame(TPixel backgroundColor) } /// - IEnumerator> IEnumerable>.GetEnumerator() => this.frames.GetEnumerator(); + public IEnumerator> GetEnumerator() + { + this.EnsureNotDisposed(); + + return this.frames.GetEnumerator(); + } /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.frames).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); private void ValidateFrame(ImageFrame frame) { @@ -408,7 +413,7 @@ protected override void Dispose(bool disposing) private ImageFrame CopyNonCompatibleFrame(ImageFrame source) { - var result = new ImageFrame( + ImageFrame result = new( this.parent.GetConfiguration(), source.Size(), source.Metadata.DeepClone()); diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 814843013e..4dab82a024 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -80,9 +80,7 @@ public Image(int width, int height) /// The images metadata. internal Image(Configuration configuration, int width, int height, ImageMetadata metadata) : base(configuration, PixelTypeInfo.Create(), metadata, width, height) - { - this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); - } + => this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); /// /// Initializes a new instance of the class @@ -115,9 +113,7 @@ internal Image( int height, ImageMetadata metadata) : base(configuration, PixelTypeInfo.Create(), metadata, width, height) - { - this.frames = new ImageFrameCollection(this, width, height, memoryGroup); - } + => this.frames = new ImageFrameCollection(this, width, height, memoryGroup); /// /// Initializes a new instance of the class @@ -135,9 +131,7 @@ internal Image( TPixel backgroundColor, ImageMetadata metadata) : base(configuration, PixelTypeInfo.Create(), metadata, width, height) - { - this.frames = new ImageFrameCollection(this, width, height, backgroundColor); - } + => this.frames = new ImageFrameCollection(this, width, height, backgroundColor); /// /// Initializes a new instance of the class @@ -148,9 +142,7 @@ internal Image( /// The frames that will be owned by this image instance. internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable> frames) : base(configuration, PixelTypeInfo.Create(), metadata, ValidateFramesAndGetSize(frames)) - { - this.frames = new ImageFrameCollection(this, frames); - } + => this.frames = new ImageFrameCollection(this, frames); /// protected override ImageFrameCollection NonGenericFrameCollection => this.Frames; @@ -181,7 +173,7 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable< /// Thrown when the provided (x,y) coordinates are outside the image boundary. public TPixel this[int x, int y] { - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { this.EnsureNotDisposed(); @@ -190,7 +182,7 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable< return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y); } - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] set { this.EnsureNotDisposed(); @@ -212,7 +204,7 @@ public void ProcessPixelRows(PixelAccessorAction processPixels) try { - var accessor = new PixelAccessor(buffer); + PixelAccessor accessor = new(buffer); processPixels(accessor); } finally @@ -243,8 +235,8 @@ public void ProcessPixelRows( try { - var accessor1 = new PixelAccessor(buffer1); - var accessor2 = new PixelAccessor(buffer2); + PixelAccessor accessor1 = new(buffer1); + PixelAccessor accessor2 = new(buffer2); processPixels(accessor1, accessor2); } finally @@ -283,9 +275,9 @@ public void ProcessPixelRows( try { - var accessor1 = new PixelAccessor(buffer1); - var accessor2 = new PixelAccessor(buffer2); - var accessor3 = new PixelAccessor(buffer3); + PixelAccessor accessor1 = new(buffer1); + PixelAccessor accessor2 = new(buffer2); + PixelAccessor accessor3 = new(buffer3); processPixels(accessor1, accessor2, accessor3); } finally @@ -348,7 +340,7 @@ public Image Clone(Configuration configuration) { this.EnsureNotDisposed(); - var clonedFrames = new ImageFrame[this.frames.Count]; + ImageFrame[] clonedFrames = new ImageFrame[this.frames.Count]; for (int i = 0; i < clonedFrames.Length; i++) { clonedFrames[i] = this.frames[i].Clone(configuration); @@ -367,7 +359,7 @@ public override Image CloneAs(Configuration configuration) { this.EnsureNotDisposed(); - var clonedFrames = new ImageFrame[this.frames.Count]; + ImageFrame[] clonedFrames = new ImageFrame[this.frames.Count]; for (int i = 0; i < clonedFrames.Length; i++) { clonedFrames[i] = this.frames[i].CloneAs(configuration); @@ -444,7 +436,7 @@ private static Size ValidateFramesAndGetSize(IEnumerable> fra return rootSize; } - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void VerifyCoords(int x, int y) { if ((uint)x >= (uint)this.Width) @@ -458,9 +450,6 @@ private void VerifyCoords(int x, int y) } } - [MethodImpl(InliningOptions.ColdPath)] private static void ThrowArgumentOutOfRangeException(string paramName) - { - throw new ArgumentOutOfRangeException(paramName); - } + => throw new ArgumentOutOfRangeException(paramName); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs index 9da1d98e3b..d5e7a99677 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs @@ -79,14 +79,15 @@ public IEnumerable> EnumeratePixelRegions(Image GetRow(int pos) } } } + + /// + public IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + long maximumPixels = Math.Min(this.MaximumPixels, (long)frame.Width * frame.Height); + long maxNumberOfRows = maximumPixels / frame.Width; + long totalNumberOfRows = frame.Height; + + if (totalNumberOfRows <= maxNumberOfRows) + { + yield return frame.PixelBuffer.GetRegion(); + } + else + { + double r = maxNumberOfRows / (double)totalNumberOfRows; + + // Use a rough approximation to make sure we don't leave out large contiguous regions: + if (maxNumberOfRows > 200) + { + r = Math.Round(r, 2); + } + else + { + r = Math.Round(r, 1); + } + + r = Math.Max(this.MinimumScanRatio, r); // always visit the minimum defined portion of the image. + + Rational ratio = new(r); + + int denom = (int)ratio.Denominator; + int num = (int)ratio.Numerator; + DebugGuard.MustBeGreaterThan(denom, 0, "Denominator must be greater than zero."); + + for (int pos = 0; pos < totalNumberOfRows; pos++) + { + int subPos = (int)((uint)pos % (uint)denom); + if (subPos < num) + { + yield return GetRow(pos); + } + } + + Buffer2DRegion GetRow(int pos) + { + int y = pos % frame.Height; + return frame.PixelBuffer.GetRegion(0, y, frame.Width, 1); + } + } + } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs index 580227c2d7..150f785b38 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs @@ -20,4 +20,11 @@ public IEnumerable> EnumeratePixelRegions(Image + public IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + yield return frame.PixelBuffer.GetRegion(); + } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs index ab118b55d4..55d56679ed 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs @@ -7,16 +7,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// -/// Provides an abstraction to enumerate pixel regions within a multi-framed . +/// Provides an abstraction to enumerate pixel regions for sampling within . /// public interface IPixelSamplingStrategy { /// - /// Enumerates pixel regions within the image as . + /// Enumerates pixel regions for all frames within the image as . /// /// The image. /// The pixel type. /// An enumeration of pixel regions. IEnumerable> EnumeratePixelRegions(Image image) where TPixel : unmanaged, IPixel; + + /// + /// Enumerates pixel regions within a single image frame as . + /// + /// The image frame. + /// The pixel type. + /// An enumeration of pixel regions. + IEnumerable> EnumeratePixelRegions(ImageFrame frame) + where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs index 04e8124037..167cf91282 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -50,7 +50,7 @@ public static IndexedImageFrame BuildPaletteAndQuantizeFrame( Guard.NotNull(quantizer, nameof(quantizer)); Guard.NotNull(source, nameof(source)); - var interest = Rectangle.Intersect(source.Bounds(), bounds); + Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds); Buffer2DRegion region = source.PixelBuffer.GetRegion(interest); // Collect the palette. Required before the second pass runs. @@ -77,9 +77,9 @@ public static IndexedImageFrame QuantizeFrame( where TPixel : unmanaged, IPixel { Guard.NotNull(source, nameof(source)); - var interest = Rectangle.Intersect(source.Bounds(), bounds); + Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds); - var destination = new IndexedImageFrame( + IndexedImageFrame destination = new( quantizer.Configuration, interest.Width, interest.Height, @@ -99,13 +99,39 @@ public static IndexedImageFrame QuantizeFrame( return destination; } - internal static void BuildPalette( + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The pixel sampling strategy. + /// The source image to sample from. + public static void BuildPalette( + this IQuantizer quantizer, + IPixelSamplingStrategy pixelSamplingStrategy, + Image source) + where TPixel : unmanaged, IPixel + { + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + quantizer.AddPaletteColors(region); + } + } + + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The pixel sampling strategy. + /// The source image frame to sample from. + public static void BuildPalette( this IQuantizer quantizer, IPixelSamplingStrategy pixelSamplingStrategy, - Image image) + ImageFrame source) where TPixel : unmanaged, IPixel { - foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(image)) + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) { quantizer.AddPaletteColors(region); } 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 void WorksWithDifferentConfigurations( 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 void DrawImageOfDifferentPixelType(TestImageProvider prov { 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 void DrawImageOfDifferentPixelType(TestImageProvider prov [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 void WorksWithDifferentLocations(TestImageProvider provider, int 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 void DrawTransformed(TestImageProvider provider) [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..18eb7708ca 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -46,7 +46,7 @@ public void EncodeGeneratedPatterns(TestImageProvider provider, 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 void EncodeGeneratedPatterns(TestImageProvider provider, // 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 void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() 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,10 +141,10 @@ public void Encode_GlobalPalette_DefaultPixelSamplingStrategy(TestImageP { using Image image = provider.GetImage(); - var encoder = new GifEncoder() + GifEncoder encoder = new() { ColorTableMode = GifColorTableMode.Global, - GlobalPixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) + PixelSamplingStrategy = new DefaultPixelSamplingStrategy(maxPixels, scanRatio) }; string testOutputFile = provider.Utility.SaveTestOutputFile( @@ -166,8 +155,7 @@ public void Encode_GlobalPalette_DefaultPixelSamplingStrategy(TestImageP 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 void Encode_GlobalPalette_DefaultPixelSamplingStrategy(TestImageP [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 void HeaderChunk_ComesFirst() // 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 void HeaderChunk_ComesFirst() 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 void EndChunk_IsLast() 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 void EndChunk_IsLast() 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 void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj) 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 void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj) break; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } @@ -110,10 +110,10 @@ public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj) 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 void Chunk_ComesBeforeIDat(object chunkTypeObj) 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 void Chunk_ComesBeforeIDat(object chunkTypeObj) dataFound = true; } - bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; } } @@ -144,18 +144,18 @@ public void Chunk_ComesBeforeIDat(object chunkTypeObj) 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 void IgnoreMetadata_WillExcludeAllAncillaryChunks() 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 void IgnoreMetadata_WillExcludeAllAncillaryChunks() 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 void ExcludeFilter_Works(object filterObj) { PngChunkType.Data, false }, { PngChunkType.End, false } }; - var excludedChunkTypes = new List(); + List excludedChunkTypes = new(); switch (chunkFilter) { case PngChunkFilter.ExcludeGammaChunk: @@ -267,15 +267,15 @@ public void ExcludeFilter_Works(object filterObj) 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 void ExcludeFilter_Works(object filterObj) 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 void ExcludeFilter_WithNone_DoesNotExcludeChunks() 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/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index 95ed17f1c7..7907597854 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -3,24 +3,21 @@ using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Writers; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Tiff; [Trait("Format", "Tiff")] public class TiffEncoderHeaderTests { - private static readonly MemoryAllocator MemoryAllocator = MemoryAllocator.Create(); - private static readonly Configuration Configuration = Configuration.Default; - private static readonly ITiffEncoderOptions Options = new TiffEncoder(); + private static readonly TiffEncoder Encoder = new(); [Fact] public void WriteHeader_WritesValidHeader() { - using var stream = new MemoryStream(); - var encoder = new TiffEncoderCore(Options, MemoryAllocator); + using MemoryStream stream = new(); + TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); - using (var writer = new TiffStreamWriter(stream)) + using (TiffStreamWriter writer = new(stream)) { long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); } @@ -31,13 +28,11 @@ public void WriteHeader_WritesValidHeader() [Fact] public void WriteHeader_ReturnsFirstIfdMarker() { - using var stream = new MemoryStream(); - var encoder = new TiffEncoderCore(Options, MemoryAllocator); + using MemoryStream stream = new(); + TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); - using (var writer = new TiffStreamWriter(stream)) - { - long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); - Assert.Equal(4, firstIfdMarker); - } + using TiffStreamWriter writer = new(stream); + long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); + Assert.Equal(4, firstIfdMarker); } } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs index 28c4f686b0..5e435ee9bb 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs @@ -14,9 +14,7 @@ public class Generic : ImageFrameCollectionTests { [Fact] public void Constructor_ShouldCreateOneFrame() - { - Assert.Equal(1, this.Collection.Count); - } + => Assert.Equal(1, this.Collection.Count); [Fact] public void AddNewFrame_FramesMustHaveSameSize() @@ -24,7 +22,7 @@ public void AddNewFrame_FramesMustHaveSameSize() ArgumentException ex = Assert.Throws( () => { - using var frame = new ImageFrame(Configuration.Default, 1, 1); + using ImageFrame frame = new(Configuration.Default, 1, 1); using ImageFrame addedFrame = this.Collection.AddFrame(frame); }); @@ -75,7 +73,7 @@ public void InsertNewFrame_FramesMustHaveSameSize() ArgumentException ex = Assert.Throws( () => { - using var frame = new ImageFrame(Configuration.Default, 1, 1); + using ImageFrame frame = new(Configuration.Default, 1, 1); using ImageFrame insertedFrame = this.Collection.InsertFrame(1, frame); }); @@ -100,8 +98,8 @@ public void Constructor_FramesMustHaveSameSize() ArgumentException ex = Assert.Throws( () => { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 1, 1); + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 1, 1); new ImageFrameCollection( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -113,8 +111,8 @@ public void Constructor_FramesMustHaveSameSize() [Fact] public void RemoveAtFrame_ThrowIfRemovingLastFrame() { - using var imageFrame = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame }); @@ -126,9 +124,9 @@ public void RemoveAtFrame_ThrowIfRemovingLastFrame() [Fact] public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist() { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -139,9 +137,9 @@ public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist() [Fact] public void RootFrameIsFrameAtIndexZero() { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -151,9 +149,9 @@ public void RootFrameIsFrameAtIndexZero() [Fact] public void ConstructorPopulatesFrames() { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -163,9 +161,9 @@ public void ConstructorPopulatesFrames() [Fact] public void DisposeClearsCollection() { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -177,9 +175,9 @@ public void DisposeClearsCollection() [Fact] public void Dispose_DisposesAllInnerFrames() { - using var imageFrame1 = new ImageFrame(Configuration.Default, 10, 10); - using var imageFrame2 = new ImageFrame(Configuration.Default, 10, 10); - var collection = new ImageFrameCollection( + using ImageFrame imageFrame1 = new(Configuration.Default, 10, 10); + using ImageFrame imageFrame2 = new(Configuration.Default, 10, 10); + ImageFrameCollection collection = new( this.Image, new[] { imageFrame1, imageFrame2 }); @@ -188,11 +186,8 @@ public void Dispose_DisposesAllInnerFrames() Assert.All( framesSnapShot, - f => - { - // The pixel source of the frame is null after its been disposed. - Assert.Null(f.PixelBuffer); - }); + f => // The pixel source of the frame is null after its been disposed. + Assert.Null(f.PixelBuffer)); } [Theory] @@ -200,18 +195,14 @@ public void Dispose_DisposesAllInnerFrames() public void CloneFrame(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image img = provider.GetImage()) - { - using var imageFrame = new ImageFrame(Configuration.Default, 10, 10); - using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway - using (Image cloned = img.Frames.CloneFrame(0)) - { - Assert.Equal(2, img.Frames.Count); - Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); - - cloned.ComparePixelBufferTo(imgMem); - } - } + using Image img = provider.GetImage(); + using ImageFrame imageFrame = new(Configuration.Default, 10, 10); + using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); // add a frame anyway + using Image cloned = img.Frames.CloneFrame(0); + Assert.Equal(2, img.Frames.Count); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + + cloned.ComparePixelBufferTo(imgMem); } [Theory] @@ -219,19 +210,15 @@ public void CloneFrame(TestImageProvider provider) public void ExtractFrame(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image img = provider.GetImage()) - { - Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMemory)); - TPixel[] sourcePixelData = imgMemory.ToArray(); - - using var imageFrame = new ImageFrame(Configuration.Default, 10, 10); - using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); - using (Image cloned = img.Frames.ExportFrame(0)) - { - Assert.Equal(1, img.Frames.Count); - cloned.ComparePixelBufferTo(sourcePixelData.AsSpan()); - } - } + using Image img = provider.GetImage(); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMemory)); + TPixel[] sourcePixelData = imgMemory.ToArray(); + + using ImageFrame imageFrame = new(Configuration.Default, 10, 10); + using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); + using Image cloned = img.Frames.ExportFrame(0); + Assert.Equal(1, img.Frames.Count); + cloned.ComparePixelBufferTo(sourcePixelData.AsSpan()); } [Fact] @@ -266,7 +253,7 @@ public void AddFrameFromPixelData() [Fact] public void AddFrame_clones_sourceFrame() { - using var otherFrame = new ImageFrame(Configuration.Default, 10, 10); + using ImageFrame otherFrame = new(Configuration.Default, 10, 10); using ImageFrame addedFrame = this.Image.Frames.AddFrame(otherFrame); Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory otherFrameMem)); @@ -277,7 +264,7 @@ public void AddFrame_clones_sourceFrame() [Fact] public void InsertFrame_clones_sourceFrame() { - using var otherFrame = new ImageFrame(Configuration.Default, 10, 10); + using ImageFrame otherFrame = new(Configuration.Default, 10, 10); using ImageFrame addedFrame = this.Image.Frames.InsertFrame(0, otherFrame); Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory otherFrameMem)); @@ -332,7 +319,7 @@ public void Contains_FalseIfNonMember() this.Image.Frames.CreateFrame(); } - using var frame = new ImageFrame(Configuration.Default, 10, 10); + using ImageFrame frame = new(Configuration.Default, 10, 10); Assert.False(this.Image.Frames.Contains(frame)); } @@ -343,14 +330,13 @@ public void PreferContiguousImageBuffers_True_AppliedToAllFrames() configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = 1000 }; configuration.PreferContiguousImageBuffers = true; - using var image = new Image(configuration, 100, 100); + using Image image = new(configuration, 100, 100); image.Frames.CreateFrame(); image.Frames.InsertFrame(0, image.Frames[0]); image.Frames.CreateFrame(Color.Red); Assert.Equal(4, image.Frames.Count); - IEnumerable> frames = image.Frames; - foreach (ImageFrame frame in frames) + foreach (ImageFrame frame in image.Frames) { Assert.True(frame.DangerousTryGetSinglePixelMemory(out Memory _)); } @@ -359,8 +345,8 @@ public void PreferContiguousImageBuffers_True_AppliedToAllFrames() [Fact] public void DisposeCall_NoThrowIfCalledMultiple() { - var image = new Image(Configuration.Default, 10, 10); - var frameCollection = image.Frames as ImageFrameCollection; + Image image = new(Configuration.Default, 10, 10); + ImageFrameCollection frameCollection = image.Frames; image.Dispose(); // this should invalidate underlying collection as well frameCollection.Dispose(); @@ -369,33 +355,33 @@ public void DisposeCall_NoThrowIfCalledMultiple() [Fact] public void PublicProperties_ThrowIfDisposed() { - var image = new Image(Configuration.Default, 10, 10); - var frameCollection = image.Frames as ImageFrameCollection; + Image image = new(Configuration.Default, 10, 10); + ImageFrameCollection frameCollection = image.Frames; image.Dispose(); // this should invalidate underlying collection as well - Assert.Throws(() => { var prop = frameCollection.RootFrame; }); + Assert.Throws(() => { ImageFrame prop = frameCollection.RootFrame; }); } [Fact] public void PublicMethods_ThrowIfDisposed() { - var image = new Image(Configuration.Default, 10, 10); - var frameCollection = image.Frames as ImageFrameCollection; + Image image = new(Configuration.Default, 10, 10); + ImageFrameCollection frameCollection = image.Frames; image.Dispose(); // this should invalidate underlying collection as well - Assert.Throws(() => { var res = frameCollection.AddFrame(default); }); - Assert.Throws(() => { var res = frameCollection.CloneFrame(default); }); - Assert.Throws(() => { var res = frameCollection.Contains(default); }); - Assert.Throws(() => { var res = frameCollection.CreateFrame(); }); - Assert.Throws(() => { var res = frameCollection.CreateFrame(default); }); - Assert.Throws(() => { var res = frameCollection.ExportFrame(default); }); - Assert.Throws(() => { var res = frameCollection.GetEnumerator(); }); - Assert.Throws(() => { var prop = frameCollection.IndexOf(default); }); - Assert.Throws(() => { var prop = frameCollection.InsertFrame(default, default); }); - Assert.Throws(() => { frameCollection.RemoveFrame(default); }); - Assert.Throws(() => { frameCollection.MoveFrame(default, default); }); + Assert.Throws(() => { ImageFrame res = frameCollection.AddFrame(default(ImageFrame)); }); + Assert.Throws(() => { Image res = frameCollection.CloneFrame(default); }); + Assert.Throws(() => { bool res = frameCollection.Contains(default); }); + Assert.Throws(() => { ImageFrame res = frameCollection.CreateFrame(); }); + Assert.Throws(() => { ImageFrame res = frameCollection.CreateFrame(default); }); + Assert.Throws(() => { Image res = frameCollection.ExportFrame(default); }); + Assert.Throws(() => { IEnumerator> res = frameCollection.GetEnumerator(); }); + Assert.Throws(() => { int prop = frameCollection.IndexOf(default); }); + Assert.Throws(() => { ImageFrame prop = frameCollection.InsertFrame(default, default); }); + Assert.Throws(() => frameCollection.RemoveFrame(default)); + Assert.Throws(() => frameCollection.MoveFrame(default, default)); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index 6e394d5925..60290a7eda 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -19,7 +19,7 @@ public class NonGeneric : ImageFrameCollectionTests [Fact] public void AddFrame_OfDifferentPixelType() { - using (var sourceImage = new Image( + using (Image sourceImage = new( this.Image.GetConfiguration(), this.Image.Width, this.Image.Height, @@ -32,7 +32,7 @@ public void AddFrame_OfDifferentPixelType() Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray(); Assert.Equal(2, this.Collection.Count); - var actualFrame = (ImageFrame)this.Collection[1]; + ImageFrame actualFrame = (ImageFrame)this.Collection[1]; actualFrame.ComparePixelBufferTo(expectedAllBlue); } @@ -40,7 +40,7 @@ public void AddFrame_OfDifferentPixelType() [Fact] public void InsertFrame_OfDifferentPixelType() { - using (var sourceImage = new Image( + using (Image sourceImage = new( this.Image.GetConfiguration(), this.Image.Width, this.Image.Height, @@ -53,25 +53,20 @@ public void InsertFrame_OfDifferentPixelType() Enumerable.Repeat((Rgba32)Color.Blue, this.Image.Width * this.Image.Height).ToArray(); Assert.Equal(2, this.Collection.Count); - var actualFrame = (ImageFrame)this.Collection[0]; + ImageFrame actualFrame = (ImageFrame)this.Collection[0]; actualFrame.ComparePixelBufferTo(expectedAllBlue); } [Fact] public void Constructor_ShouldCreateOneFrame() - { - Assert.Equal(1, this.Collection.Count); - } + => Assert.Equal(1, this.Collection.Count); [Fact] public void AddNewFrame_FramesMustHaveSameSize() { ArgumentException ex = Assert.Throws( - () => - { - this.Collection.AddFrame(new ImageFrame(Configuration.Default, 1, 1)); - }); + () => this.Collection.AddFrame(new ImageFrame(Configuration.Default, 1, 1))); Assert.StartsWith("Frame must have the same dimensions as the image.", ex.Message); } @@ -80,10 +75,7 @@ public void AddNewFrame_FramesMustHaveSameSize() public void AddNewFrame_Frame_FramesNotBeNull() { ArgumentNullException ex = Assert.Throws( - () => - { - this.Collection.AddFrame(null); - }); + () => this.Collection.AddFrame(null)); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); } @@ -92,10 +84,7 @@ public void AddNewFrame_Frame_FramesNotBeNull() public void InsertNewFrame_FramesMustHaveSameSize() { ArgumentException ex = Assert.Throws( - () => - { - this.Collection.InsertFrame(1, new ImageFrame(Configuration.Default, 1, 1)); - }); + () => this.Collection.InsertFrame(1, new ImageFrame(Configuration.Default, 1, 1))); Assert.StartsWith("Frame must have the same dimensions as the image.", ex.Message); } @@ -104,10 +93,7 @@ public void InsertNewFrame_FramesMustHaveSameSize() public void InsertNewFrame_FramesNotBeNull() { ArgumentNullException ex = Assert.Throws( - () => - { - this.Collection.InsertFrame(1, null); - }); + () => this.Collection.InsertFrame(1, null)); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); } @@ -116,10 +102,7 @@ public void InsertNewFrame_FramesNotBeNull() public void RemoveAtFrame_ThrowIfRemovingLastFrame() { InvalidOperationException ex = Assert.Throws( - () => - { - this.Collection.RemoveFrame(0); - }); + () => this.Collection.RemoveFrame(0)); Assert.Equal("Cannot remove last frame.", ex.Message); } @@ -134,30 +117,24 @@ public void RemoveAtFrame_CanRemoveFrameZeroIfMultipleFramesExist() [Fact] public void RootFrameIsFrameAtIndexZero() - { - Assert.Equal(this.Collection.RootFrame, this.Collection[0]); - } + => Assert.Equal(this.Collection.RootFrame, this.Collection[0]); [Theory] [WithTestPatternImages(10, 10, PixelTypes.Rgba32 | PixelTypes.Bgr24)] public void CloneFrame(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image img = provider.GetImage()) - { - ImageFrameCollection nonGenericFrameCollection = img.Frames; + using Image img = provider.GetImage(); + ImageFrameCollection nonGenericFrameCollection = img.Frames; - nonGenericFrameCollection.AddFrame(new ImageFrame(Configuration.Default, 10, 10)); // add a frame anyway - using (Image cloned = nonGenericFrameCollection.CloneFrame(0)) - { - Assert.Equal(2, img.Frames.Count); + nonGenericFrameCollection.AddFrame(new ImageFrame(Configuration.Default, 10, 10)); // add a frame anyway + using Image cloned = nonGenericFrameCollection.CloneFrame(0); + Assert.Equal(2, img.Frames.Count); - var expectedClone = (Image)cloned; + Image expectedClone = (Image)cloned; - Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); - expectedClone.ComparePixelBufferTo(imgMem); - } - } + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + expectedClone.ComparePixelBufferTo(imgMem); } [Theory] @@ -165,22 +142,18 @@ public void CloneFrame(TestImageProvider provider) public void ExtractFrame(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image img = provider.GetImage()) - { - Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); - TPixel[] sourcePixelData = imgMem.ToArray(); + using Image img = provider.GetImage(); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + TPixel[] sourcePixelData = imgMem.ToArray(); - ImageFrameCollection nonGenericFrameCollection = img.Frames; + ImageFrameCollection nonGenericFrameCollection = img.Frames; - nonGenericFrameCollection.AddFrame(new ImageFrame(Configuration.Default, 10, 10)); - using (Image cloned = nonGenericFrameCollection.ExportFrame(0)) - { - Assert.Equal(1, img.Frames.Count); + nonGenericFrameCollection.AddFrame(new ImageFrame(Configuration.Default, 10, 10)); + using Image cloned = nonGenericFrameCollection.ExportFrame(0); + Assert.Equal(1, img.Frames.Count); - var expectedClone = (Image)cloned; - expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan()); - } - } + Image expectedClone = (Image)cloned; + expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan()); } [Fact] @@ -190,7 +163,7 @@ public void CreateFrame_Default() Assert.Equal(2, this.Image.Frames.Count); - var frame = (ImageFrame)this.Image.Frames[1]; + ImageFrame frame = (ImageFrame)this.Image.Frames[1]; frame.ComparePixelBufferTo(default(Rgba32)); } @@ -202,7 +175,7 @@ public void CreateFrame_CustomFillColor() Assert.Equal(2, this.Image.Frames.Count); - var frame = (ImageFrame)this.Image.Frames[1]; + ImageFrame frame = (ImageFrame)this.Image.Frames[1]; frame.ComparePixelBufferTo(Color.HotPink); } @@ -210,132 +183,127 @@ public void CreateFrame_CustomFillColor() [Fact] public void MoveFrame_LeavesFrameInCorrectLocation() { - for (var i = 0; i < 9; i++) + for (int i = 0; i < 9; i++) { this.Image.Frames.CreateFrame(); } - var frame = this.Image.Frames[4]; + ImageFrame frame = this.Image.Frames[4]; this.Image.Frames.MoveFrame(4, 7); - var newIndex = this.Image.Frames.IndexOf(frame); + int newIndex = this.Image.Frames.IndexOf(frame); Assert.Equal(7, newIndex); } [Fact] public void IndexOf_ReturnsCorrectIndex() { - for (var i = 0; i < 9; i++) + for (int i = 0; i < 9; i++) { this.Image.Frames.CreateFrame(); } - var frame = this.Image.Frames[4]; - var index = this.Image.Frames.IndexOf(frame); + ImageFrame frame = this.Image.Frames[4]; + int index = this.Image.Frames.IndexOf(frame); Assert.Equal(4, index); } [Fact] public void Contains_TrueIfMember() { - for (var i = 0; i < 9; i++) + for (int i = 0; i < 9; i++) { this.Image.Frames.CreateFrame(); } - var frame = this.Image.Frames[4]; + ImageFrame frame = this.Image.Frames[4]; Assert.True(this.Image.Frames.Contains(frame)); } [Fact] public void Contains_FalseIfNonMember() { - for (var i = 0; i < 9; i++) + for (int i = 0; i < 9; i++) { this.Image.Frames.CreateFrame(); } - var frame = new ImageFrame(Configuration.Default, 10, 10); + ImageFrame frame = new(Configuration.Default, 10, 10); Assert.False(this.Image.Frames.Contains(frame)); } [Fact] public void PublicProperties_ThrowIfDisposed() { - var image = new Image(Configuration.Default, 10, 10); - var frameCollection = image.Frames; + Image image = new(Configuration.Default, 10, 10); + ImageFrameCollection frameCollection = image.Frames; image.Dispose(); // this should invalidate underlying collection as well - Assert.Throws(() => { var prop = frameCollection.RootFrame; }); + Assert.Throws(() => { ImageFrame prop = frameCollection.RootFrame; }); } [Fact] public void PublicMethods_ThrowIfDisposed() { - var image = new Image(Configuration.Default, 10, 10); - var frameCollection = image.Frames; - var rgba32Array = new Rgba32[0]; + Image image = new(Configuration.Default, 10, 10); + ImageFrameCollection frameCollection = image.Frames; + Rgba32[] rgba32Array = Array.Empty(); image.Dispose(); // this should invalidate underlying collection as well - Assert.Throws(() => { var res = frameCollection.AddFrame((ImageFrame)null); }); - Assert.Throws(() => { var res = frameCollection.AddFrame(rgba32Array); }); - Assert.Throws(() => { var res = frameCollection.AddFrame((ImageFrame)null); }); - Assert.Throws(() => { var res = frameCollection.AddFrame(rgba32Array.AsSpan()); }); - Assert.Throws(() => { var res = frameCollection.CloneFrame(default); }); - Assert.Throws(() => { var res = frameCollection.Contains(default); }); - Assert.Throws(() => { var res = frameCollection.CreateFrame(); }); - Assert.Throws(() => { var res = frameCollection.CreateFrame(default); }); - Assert.Throws(() => { var res = frameCollection.ExportFrame(default); }); - Assert.Throws(() => { var res = frameCollection.GetEnumerator(); }); - Assert.Throws(() => { var prop = frameCollection.IndexOf(default); }); - Assert.Throws(() => { var prop = frameCollection.InsertFrame(default, default); }); - Assert.Throws(() => { frameCollection.RemoveFrame(default); }); - Assert.Throws(() => { frameCollection.MoveFrame(default, default); }); + Assert.Throws(() => { ImageFrame res = frameCollection.AddFrame((ImageFrame)null); }); + Assert.Throws(() => { ImageFrame res = frameCollection.AddFrame(rgba32Array); }); + Assert.Throws(() => { ImageFrame res = frameCollection.AddFrame((ImageFrame)null); }); + Assert.Throws(() => { ImageFrame res = frameCollection.AddFrame(rgba32Array.AsSpan()); }); + Assert.Throws(() => { Image res = frameCollection.CloneFrame(default); }); + Assert.Throws(() => { bool res = frameCollection.Contains(default); }); + Assert.Throws(() => { ImageFrame res = frameCollection.CreateFrame(); }); + Assert.Throws(() => { ImageFrame res = frameCollection.CreateFrame(default); }); + Assert.Throws(() => { Image res = frameCollection.ExportFrame(default); }); + Assert.Throws(() => { IEnumerator> res = frameCollection.GetEnumerator(); }); + Assert.Throws(() => { int prop = frameCollection.IndexOf(default); }); + Assert.Throws(() => { ImageFrame prop = frameCollection.InsertFrame(default, default); }); + Assert.Throws(() => frameCollection.RemoveFrame(default)); + Assert.Throws(() => frameCollection.MoveFrame(default, default)); } /// /// Integration test for end-to end API validation. /// /// The pixel type of the image. + /// The test image provider [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] public void ConstructGif_FromDifferentPixelTypes(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image source = provider.GetImage()) - using (var dest = new Image(source.GetConfiguration(), source.Width, source.Height)) + using Image source = provider.GetImage(); + using Image dest = new(source.GetConfiguration(), source.Width, source.Height); + // Giphy.gif has 5 frames + ImportFrameAs(source.Frames, dest.Frames, 0); + ImportFrameAs(source.Frames, dest.Frames, 1); + ImportFrameAs(source.Frames, dest.Frames, 2); + ImportFrameAs(source.Frames, dest.Frames, 3); + ImportFrameAs(source.Frames, dest.Frames, 4); + + // Drop the original empty root frame: + dest.Frames.RemoveFrame(0); + + dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); + dest.CompareToOriginal(provider); + + for (int i = 0; i < 5; i++) { - // Giphy.gif has 5 frames - ImportFrameAs(source.Frames, dest.Frames, 0); - ImportFrameAs(source.Frames, dest.Frames, 1); - ImportFrameAs(source.Frames, dest.Frames, 2); - ImportFrameAs(source.Frames, dest.Frames, 3); - ImportFrameAs(source.Frames, dest.Frames, 4); - - // Drop the original empty root frame: - dest.Frames.RemoveFrame(0); - - dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); - dest.CompareToOriginal(provider); - - for (int i = 0; i < 5; i++) - { - CompareGifMetadata(source.Frames[i], dest.Frames[i]); - } + CompareGifMetadata(source.Frames[i], dest.Frames[i]); } } private static void ImportFrameAs(ImageFrameCollection source, ImageFrameCollection destination, int index) where TPixel : unmanaged, IPixel { - using (Image temp = source.CloneFrame(index)) - { - using (Image temp2 = temp.CloneAs()) - { - destination.AddFrame(temp2.Frames.RootFrame); - } - } + using Image temp = source.CloneFrame(index); + using Image temp2 = temp.CloneAs(); + destination.AddFrame(temp2.Frames.RootFrame); } private static void CompareGifMetadata(ImageFrame a, ImageFrame b) diff --git a/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs index f42726412d..20eb91aa40 100644 --- a/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs +++ b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs @@ -10,24 +10,36 @@ namespace SixLabors.ImageSharp.Tests.Quantization; public class PixelSamplingStrategyTests { - public static readonly TheoryData DefaultPixelSamplingStrategy_Data = new TheoryData() - { - { 100, 100, 1, 10000 }, - { 100, 100, 1, 5000 }, - { 100, 100, 10, 50000 }, - { 99, 100, 11, 30000 }, - { 97, 99, 11, 80000 }, - { 99, 100, 11, 20000 }, - { 99, 501, 20, 100000 }, - { 97, 500, 20, 10000 }, - { 103, 501, 20, 1000 }, - }; + public static readonly TheoryData DefaultPixelSamplingStrategy_MultiFrame_Data = new() + { + { 100, 100, 1, 10000 }, + { 100, 100, 1, 5000 }, + { 100, 100, 10, 50000 }, + { 99, 100, 11, 30000 }, + { 97, 99, 11, 80000 }, + { 99, 100, 11, 20000 }, + { 99, 501, 20, 100000 }, + { 97, 500, 20, 10000 }, + { 103, 501, 20, 1000 }, + }; + + public static readonly TheoryData DefaultPixelSamplingStrategy_Data = new() + { + { 100, 100, 9900 }, + { 100, 100, 5000 }, + { 99, 100, 30000 }, + { 97, 99, 80000 }, + { 99, 100, 20000 }, + { 99, 501, 100000 }, + { 97, 500, 10000 }, + { 103, 501, 1000 }, + }; [Fact] - public void ExtensivePixelSamplingStrategy_EnumeratesAll() + public void ExtensivePixelSamplingStrategy_EnumeratesAll_MultiFrame() { using Image image = CreateTestImage(100, 100, 100); - var strategy = new ExtensivePixelSamplingStrategy(); + ExtensivePixelSamplingStrategy strategy = new(); foreach (Buffer2DRegion region in strategy.EnumeratePixelRegions(image)) { @@ -39,13 +51,32 @@ public void ExtensivePixelSamplingStrategy_EnumeratesAll() ImageComparer.Exact.VerifySimilarity(expected, image); } + [Fact] + public void ExtensivePixelSamplingStrategy_EnumeratesAll() + { + using Image image = CreateTestImage(100, 100, 100); + ExtensivePixelSamplingStrategy strategy = new(); + + foreach (ImageFrame frame in image.Frames) + { + foreach (Buffer2DRegion region in strategy.EnumeratePixelRegions(frame)) + { + PaintWhite(region); + } + } + + using Image expected = CreateTestImage(100, 100, 100, true); + + ImageComparer.Exact.VerifySimilarity(expected, image); + } + [Theory] - [WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)] - public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels) + [WithBlankImages(nameof(DefaultPixelSamplingStrategy_MultiFrame_Data), 1, 1, PixelTypes.L8)] + public void DefaultPixelSamplingStrategy_IsFair_MultiFrame(TestImageProvider dummyProvider, int width, int height, int noOfFrames, int maximumNumberOfPixels) { using Image image = CreateTestImage(width, height, noOfFrames); - var strategy = new DefaultPixelSamplingStrategy(maximumNumberOfPixels, 0.1); + DefaultPixelSamplingStrategy strategy = new(maximumNumberOfPixels, 0.1); long visitedPixels = 0; foreach (Buffer2DRegion region in strategy.EnumeratePixelRegions(image)) @@ -67,9 +98,40 @@ public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider dummyProvi Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}"); } + [Theory] + [WithBlankImages(nameof(DefaultPixelSamplingStrategy_Data), 1, 1, PixelTypes.L8)] + public void DefaultPixelSamplingStrategy_IsFair(TestImageProvider dummyProvider, int width, int height, int maximumNumberOfPixels) + { + using Image image = CreateTestImage(width, height, 1); + + DefaultPixelSamplingStrategy strategy = new(maximumNumberOfPixels, 0.1); + + long visitedPixels = 0; + foreach (ImageFrame frame in image.Frames) + { + foreach (Buffer2DRegion region in strategy.EnumeratePixelRegions(frame)) + { + PaintWhite(region); + visitedPixels += region.Width * region.Height; + } + } + + image.DebugSave( + dummyProvider, + $"W{width}_H{height}_maximumNumberOfPixels_{maximumNumberOfPixels}", + appendPixelTypeToFileName: false); + + int maximumPixels = image.Width * image.Height * image.Frames.Count / 10; + maximumPixels = Math.Max(maximumPixels, (int)strategy.MaximumPixels); + + // allow some inaccuracy: + double visitRatio = visitedPixels / (double)maximumPixels; + Assert.True(visitRatio <= 1.1, $"{visitedPixels}>{maximumPixels}"); + } + private static void PaintWhite(Buffer2DRegion region) { - var white = new L8(255); + L8 white = new(255); for (int y = 0; y < region.Height; y++) { region.DangerousGetRowSpan(y).Fill(white); @@ -79,7 +141,7 @@ private static void PaintWhite(Buffer2DRegion region) private static Image CreateTestImage(int width, int height, int noOfFrames, bool paintWhite = false) { L8 bg = paintWhite ? new L8(255) : default; - var image = new Image(width, height, bg); + Image image = new(width, height, bg); for (int i = 1; i < noOfFrames; i++) { 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 void Encode(Image image, Stream stream) /// 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 async Task EncodeAsync(Image image, Stream stream, Cancel // 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); } }