From 8dd4f35d5e126e0f429e27a9ba8f44b1445cebc0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 25 Nov 2024 21:56:54 +1000 Subject: [PATCH 1/2] Normalize handling of transparent pixels with color components on encode. --- .../Formats/AlphaAwareImageEncoder.cs | 15 ++ src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 227 +++++++++++++----- src/ImageSharp/Formats/EncodingUtilities.cs | 97 ++++++++ src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 115 +++++---- .../Formats/IAnimatedImageEncoder.cs | 2 +- .../Formats/IQuantizingImageEncoder.cs | 2 +- .../Formats/Icon/IconEncoderCore.cs | 4 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 6 - src/ImageSharp/Formats/Png/PngEncoderCore.cs | 168 ++++++------- .../Formats/Png/PngTransparentColorMode.cs | 21 -- src/ImageSharp/Formats/Qoi/QoiEncoder.cs | 2 +- src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs | 219 +++++++++-------- src/ImageSharp/Formats/Tga/TgaEncoder.cs | 2 +- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 32 ++- .../Formats/Tiff/TiffEncoderCore.cs | 48 +++- .../Formats/TransparentColorMode.cs | 22 ++ src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 2 +- .../Formats/Webp/Lossless/PredictorEncoder.cs | 12 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 7 - .../Formats/Webp/WebpEncoderCore.cs | 16 +- .../Formats/Webp/WebpTransparentColorMode.cs | 20 -- src/ImageSharp/ImageFrame.cs | 2 +- src/ImageSharp/ImageFrame{TPixel}.cs | 2 +- src/ImageSharp/Memory/Buffer2DExtensions.cs | 62 ++++- src/ImageSharp/Memory/Buffer2DRegion{T}.cs | 2 +- .../MemoryGroupExtensions.cs | 7 +- .../DefaultImageProcessorContext{TPixel}.cs | 12 +- .../ProcessingExtensions.IntegralImage.cs | 5 +- .../AdaptiveThresholdProcessor{TPixel}.cs | 19 +- .../BinaryThresholdProcessor{TPixel}.cs | 10 +- .../Convolution/BokehBlurProcessor{TPixel}.cs | 22 +- .../Convolution2DProcessor{TPixel}.cs | 2 +- .../Convolution2PassProcessor{TPixel}.cs | 2 +- .../ConvolutionProcessor{TPixel}.cs | 2 +- .../EdgeDetectorCompassProcessor{TPixel}.cs | 2 +- .../MedianBlurProcessor{TPixel}.cs | 2 +- .../PaletteDitherProcessor{TPixel}.cs | 2 +- .../Effects/PixelRowDelegateProcessor.cs | 8 +- ...lRowDelegateProcessor{TPixel,TDelegate}.cs | 4 +- .../Effects/PixelateProcessor{TPixel}.cs | 2 +- .../Filters/FilterProcessor{TPixel}.cs | 8 +- .../Filters/OpaqueProcessor{TPixel}.cs | 4 +- .../AutoLevelProcessor{TPixel}.cs | 29 +-- ...lHistogramEqualizationProcessor{TPixel}.cs | 10 +- .../BackgroundColorProcessor{TPixel}.cs | 4 +- .../Overlays/GlowProcessor{TPixel}.cs | 4 +- .../Overlays/VignetteProcessor{TPixel}.cs | 4 +- .../Quantization/QuantizeProcessor{TPixel}.cs | 2 +- .../Quantization/QuantizerUtilities.cs | 71 +++++- .../AffineTransformProcessor{TPixel}.cs | 10 +- .../Linear/FlipProcessor{TPixel}.cs | 4 +- .../ProjectiveTransformProcessor{TPixel}.cs | 10 +- .../Linear/RotateProcessor{TPixel}.cs | 16 +- .../Codecs/Webp/EncodeWebp.cs | 3 +- .../Formats/Png/PngEncoderTests.cs | 2 +- .../Formats/WebP/WebpEncoderTests.cs | 2 +- .../BaseImageOperationsExtensionTest.cs | 1 - .../Quantization/QuantizedImageTests.cs | 84 +++---- .../Quantization/WuQuantizerTests.cs | 116 +++++---- 62 files changed, 957 insertions(+), 643 deletions(-) create mode 100644 src/ImageSharp/Formats/AlphaAwareImageEncoder.cs create mode 100644 src/ImageSharp/Formats/EncodingUtilities.cs delete mode 100644 src/ImageSharp/Formats/Png/PngTransparentColorMode.cs create mode 100644 src/ImageSharp/Formats/TransparentColorMode.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs diff --git a/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs b/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs new file mode 100644 index 0000000000..f753e7282b --- /dev/null +++ b/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency. +/// +public abstract class AlphaAwareImageEncoder : ImageEncoder +{ + /// + /// Gets or initializes the mode that determines how transparent pixels are handled during encoding. + /// + public TransparentColorMode TransparentColorMode { get; init; } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7c92d3e463..321a559b1e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -91,6 +91,11 @@ internal sealed class BmpEncoderCore /// private readonly IPixelSamplingStrategy pixelSamplingStrategy; + /// + /// The transparent color mode. + /// + private readonly TransparentColorMode transparentColorMode; + /// private readonly bool processedAlphaMask; @@ -113,6 +118,7 @@ public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator) // TODO: Use a palette quantizer if supplied. this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.transparentColorMode = encoder.TransparentColorMode; this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; this.processedAlphaMask = encoder.ProcessedAlphaMask; this.skipFileHeader = encoder.SkipFileHeader; @@ -181,14 +187,14 @@ public void Encode(Image image, Stream stream, CancellationToken Span buffer = stackalloc byte[infoHeaderSize]; - // for ico/cur encoder. + // For ico/cur encoder. if (!this.skipFileHeader) { WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); } this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); - this.WriteImage(configuration, stream, image); + this.WriteImage(configuration, stream, image, cancellationToken); WriteColorProfile(stream, iccProfileData, buffer, basePosition); stream.Flush(); @@ -345,44 +351,65 @@ private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span /// /// The containing pixel data. /// - private void WriteImage(Configuration configuration, Stream stream, Image image) + /// The token to monitor for cancellation requests. + private void WriteImage( + Configuration configuration, + Stream stream, + Image image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; - switch (this.bitsPerPixel) + ImageFrame? clonedFrame = null; + try { - case BmpBitsPerPixel.Bit32: - this.Write32BitPixelData(configuration, stream, pixels); - break; + if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent); + } - case BmpBitsPerPixel.Bit24: - this.Write24BitPixelData(configuration, stream, pixels); - break; + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + Buffer2D pixels = encodingFrame.PixelBuffer; - case BmpBitsPerPixel.Bit16: - this.Write16BitPixelData(configuration, stream, pixels); - break; + switch (this.bitsPerPixel) + { + case BmpBitsPerPixel.Bit32: + this.Write32BitPixelData(configuration, stream, pixels, cancellationToken); + break; - case BmpBitsPerPixel.Bit8: - this.Write8BitPixelData(configuration, stream, image); - break; + case BmpBitsPerPixel.Bit24: + this.Write24BitPixelData(configuration, stream, pixels, cancellationToken); + break; - case BmpBitsPerPixel.Bit4: - this.Write4BitPixelData(configuration, stream, image); - break; + case BmpBitsPerPixel.Bit16: + this.Write16BitPixelData(configuration, stream, pixels, cancellationToken); + break; - case BmpBitsPerPixel.Bit2: - this.Write2BitPixelData(configuration, stream, image); - break; + case BmpBitsPerPixel.Bit8: + this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; - case BmpBitsPerPixel.Bit1: - this.Write1BitPixelData(configuration, stream, image); - break; - } + case BmpBitsPerPixel.Bit4: + this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + + case BmpBitsPerPixel.Bit2: + this.Write2BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; - if (this.processedAlphaMask) + case BmpBitsPerPixel.Bit1: + this.Write1BitPixelData(configuration, stream, encodingFrame, cancellationToken); + break; + } + + if (this.processedAlphaMask) + { + ProcessedAlphaMask(stream, encodingFrame); + } + } + finally { - ProcessedAlphaMask(stream, image); + clonedFrame?.Dispose(); } } @@ -396,7 +423,12 @@ private IMemoryOwner AllocateRow(int width, int bytesPerPixel) /// The global configuration. /// The to write to. /// The containing pixel data. - private void Write32BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels) + /// The token to monitor for cancellation requests. + private void Write32BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using IMemoryOwner row = this.AllocateRow(pixels.Width, 4); @@ -404,6 +436,8 @@ private void Write32BitPixelData(Configuration configuration, Stream str for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra32Bytes( configuration, @@ -421,7 +455,12 @@ private void Write32BitPixelData(Configuration configuration, Stream str /// The global configuration. /// The to write to. /// The containing pixel data. - private void Write24BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels) + /// The token to monitor for cancellation requests. + private void Write24BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -431,6 +470,8 @@ private void Write24BitPixelData(Configuration configuration, Stream str for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgr24Bytes( configuration, @@ -448,7 +489,12 @@ private void Write24BitPixelData(Configuration configuration, Stream str /// The global configuration. /// The to write to. /// The containing pixel data. - private void Write16BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels) + /// The token to monitor for cancellation requests. + private void Write16BitPixelData( + Configuration configuration, + Stream stream, + Buffer2D pixels, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -458,6 +504,8 @@ private void Write16BitPixelData(Configuration configuration, Stream str for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra5551Bytes( @@ -476,21 +524,32 @@ private void Write16BitPixelData(Configuration configuration, Stream str /// The type of the pixel. /// The global configuration. /// The to write to. - /// The containing pixel data. - private void Write8BitPixelData(Configuration configuration, Stream stream, Image image) + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write8BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - bool isL8 = typeof(TPixel) == typeof(L8); + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + bool is8BitLuminance = + info.BitsPerPixel == 8 + && info.ColorType == PixelColorType.Luminance + && info.AlphaRepresentation == PixelAlphaRepresentation.None + && info.ComponentInfo!.Value.ComponentCount == 1; + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize8Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); - if (isL8) + if (is8BitLuminance) { - this.Write8BitPixelData(stream, image, colorPalette); + this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken); } else { - this.Write8BitColor(configuration, stream, image, colorPalette); + this.Write8BitColor(configuration, stream, encodingFrame, colorPalette, cancellationToken); } } @@ -500,21 +559,29 @@ private void Write8BitPixelData(Configuration configuration, Stream stre /// The type of the pixel. /// The global configuration. /// 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(Configuration configuration, Stream stream, Image image, Span colorPalette) + /// The token to monitor for cancellation requests. + private void Write8BitColor( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + Span colorPalette, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration); - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); - for (int y = image.Height - 1; y >= 0; y--) + for (int y = encodingFrame.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + ReadOnlySpan pixelSpan = quantized.DangerousGetRowSpan(y); stream.Write(pixelSpan); @@ -530,9 +597,14 @@ private void Write8BitColor(Configuration configuration, Stream stream, /// /// 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 Write8BitPixelData(Stream stream, Image image, Span colorPalette) + /// The token to monitor for cancellation requests. + private void Write8BitLuminancePixelData( + Stream stream, + ImageFrame encodingFrame, + Span colorPalette, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // Create a color palette with 256 different gray values. @@ -549,9 +621,11 @@ private void Write8BitPixelData(Stream stream, Image image, Span } stream.Write(colorPalette); - Buffer2D imageBuffer = image.GetRootFramePixelBuffer(); - for (int y = image.Height - 1; y >= 0; y--) + Buffer2D imageBuffer = encodingFrame.PixelBuffer; + for (int y = encodingFrame.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + ReadOnlySpan inputPixelRow = imageBuffer.DangerousGetRowSpan(y); ReadOnlySpan outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow); stream.Write(outputPixelRow); @@ -569,8 +643,13 @@ private void Write8BitPixelData(Stream stream, Image image, Span /// The type of the pixel. /// The global configuration. /// The to write to. - /// The containing pixel data. - private void Write4BitPixelData(Configuration configuration, Stream stream, Image image) + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write4BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions() @@ -580,9 +659,9 @@ private void Write4BitPixelData(Configuration configuration, Stream stre DitherScale = this.quantizer.Options.DitherScale }); - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize4Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); @@ -591,8 +670,10 @@ private void Write4BitPixelData(Configuration configuration, Stream stre ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; - for (int y = image.Height - 1; y >= 0; y--) + for (int y = encodingFrame.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + pixelRowSpan = quantized.DangerousGetRowSpan(y); int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1; @@ -619,8 +700,13 @@ private void Write4BitPixelData(Configuration configuration, Stream stre /// The type of the pixel. /// The global configuration. /// The to write to. - /// The containing pixel data. - private void Write2BitPixelData(Configuration configuration, Stream stream, Image image) + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write2BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions() @@ -630,9 +716,9 @@ private void Write2BitPixelData(Configuration configuration, Stream stre DitherScale = this.quantizer.Options.DitherScale }); - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize2Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); @@ -641,8 +727,10 @@ private void Write2BitPixelData(Configuration configuration, Stream stre ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding; - for (int y = image.Height - 1; y >= 0; y--) + for (int y = encodingFrame.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + pixelRowSpan = quantized.DangerousGetRowSpan(y); int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4; @@ -678,8 +766,13 @@ private void Write2BitPixelData(Configuration configuration, Stream stre /// The type of the pixel. /// The global configuration. /// The to write to. - /// The containing pixel data. - private void Write1BitPixelData(Configuration configuration, Stream stream, Image image) + /// The containing pixel data. + /// The token to monitor for cancellation requests. + private void Write1BitPixelData( + Configuration configuration, + Stream stream, + ImageFrame encodingFrame, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions() @@ -689,9 +782,9 @@ private void Write1BitPixelData(Configuration configuration, Stream stre DitherScale = this.quantizer.Options.DitherScale }); - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); + frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame); - using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize1Bit, AllocationOptions.Clean); Span colorPalette = colorPaletteBuffer.GetSpan(); @@ -700,8 +793,10 @@ private void Write1BitPixelData(Configuration configuration, Stream stre ReadOnlySpan quantizedPixelRow = quantized.DangerousGetRowSpan(0); int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; - for (int y = image.Height - 1; y >= 0; y--) + for (int y = encodingFrame.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + quantizedPixelRow = quantized.DangerousGetRowSpan(y); int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8; @@ -766,10 +861,10 @@ private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, Re stream.WriteByte(indices); } - private static void ProcessedAlphaMask(Stream stream, Image image) + private static void ProcessedAlphaMask(Stream stream, ImageFrame encodingFrame) where TPixel : unmanaged, IPixel { - int arrayWidth = image.Width / 8; + int arrayWidth = encodingFrame.Width / 8; int padding = arrayWidth % 4; if (padding is not 0) { @@ -777,10 +872,10 @@ private static void ProcessedAlphaMask(Stream stream, Image imag } Span mask = stackalloc byte[arrayWidth]; - for (int y = image.Height - 1; y >= 0; y--) + for (int y = encodingFrame.Height - 1; y >= 0; y--) { mask.Clear(); - Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); + Span row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y); for (int i = 0; i < arrayWidth; i++) { diff --git a/src/ImageSharp/Formats/EncodingUtilities.cs b/src/ImageSharp/Formats/EncodingUtilities.cs new file mode 100644 index 0000000000..a979fdf6fa --- /dev/null +++ b/src/ImageSharp/Formats/EncodingUtilities.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Provides utilities for encoding images. +/// +internal static class EncodingUtilities +{ + public static bool ShouldClearTransparentPixels(TransparentColorMode mode) + where TPixel : unmanaged, IPixel + => mode == TransparentColorMode.Clear && + TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; + + /// + /// Convert transparent pixels, to pixels represented by , which can yield + /// to better compression in some cases. + /// + /// The type of the pixel. + /// The cloned where the transparent pixels will be changed. + /// The color to replace transparent pixels with. + public static void ClearTransparentPixels(ImageFrame clone, Color color) + where TPixel : unmanaged, IPixel + { + Buffer2DRegion buffer = clone.PixelBuffer.GetRegion(); + ClearTransparentPixels(clone.Configuration, ref buffer, color); + } + + /// + /// Convert transparent pixels, to pixels represented by , which can yield + /// to better compression in some cases. + /// + /// The type of the pixel. + /// The configuration. + /// The cloned where the transparent pixels will be changed. + /// The color to replace transparent pixels with. + public static void ClearTransparentPixels( + Configuration configuration, + ref Buffer2DRegion clone, + Color color) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner vectors = configuration.MemoryAllocator.Allocate(clone.Width); + Span vectorsSpan = vectors.GetSpan(); + Vector4 replacement = color.ToScaledVector4(); + for (int y = 0; y < clone.Height; y++) + { + Span span = clone.DangerousGetRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale); + ClearTransparentPixelRow(vectorsSpan, replacement); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale); + } + } + + private static void ClearTransparentPixelRow( + Span vectorsSpan, + Vector4 replacement) + { + if (Vector128.IsHardwareAccelerated) + { + Vector128 replacement128 = replacement.AsVector128(); + + for (int i = 0; i < vectorsSpan.Length; i++) + { + ref Vector4 v = ref vectorsSpan[i]; + Vector128 v128 = v.AsVector128(); + + // Do `vector == 0` + Vector128 mask = Vector128.Equals(v128, Vector128.Zero); + + // Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise) + mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3)); + + // Use the mask to select the replacement vector + // (replacement & mask) | (v128 & ~mask) + v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4(); + } + } + else + { + for (int i = 0; i < vectorsSpan.Length; i++) + { + if (vectorsSpan[i].W == 0F) + { + vectorsSpan[i] = replacement; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index a99b5862dd..3d6990478d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -665,7 +665,7 @@ private void RestoreToBackground(ImageFrame frame) return; } - Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); + Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value); Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); pixelRegion.Clear(); diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 0ed7e8c98d..3c6e269e43 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -67,6 +67,8 @@ internal sealed class GifEncoderCore /// private readonly ushort? repeatCount; + private readonly TransparentColorMode transparentColorMode; + /// /// Initializes a new instance of the class. /// @@ -83,6 +85,7 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder) this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.backgroundColor = encoder.BackgroundColor; this.repeatCount = encoder.RepeatCount; + this.transparentColorMode = encoder.TransparentColorMode; } /// @@ -131,18 +134,40 @@ public void Encode(Image image, Stream stream, CancellationToken } } + // Quantize the first frame. Checking to see whether we can clear the transparent pixels + // to allow for a smaller color palette and encoded result. using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration)) { + ImageFrame? clonedFrame = null; + Configuration configuration = this.configuration; + TransparentColorMode mode = this.transparentColorMode; + IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; + if (EncodingUtilities.ShouldClearTransparentPixels(mode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + + GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata(); + Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + EncodingUtilities.ClearTransparentPixels(clonedFrame, background); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + if (useGlobalTable) { - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); - quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + frameQuantizer.BuildPalette(configuration, mode, strategy, image); + quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds); } else { - frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame); - quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame); + quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds); } + + clonedFrame?.Dispose(); } // Write the header. @@ -236,52 +261,49 @@ private void EncodeAdditionalFrames( // This frame is reused to store de-duplicated pixel buffers. using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size); - for (int i = 1; i < image.Frames.Count; i++) + try { - if (cancellationToken.IsCancellationRequested) + for (int i = 1; i < image.Frames.Count; i++) { - if (hasPaletteQuantizer) - { - paletteQuantizer.Dispose(); - } + cancellationToken.ThrowIfCancellationRequested(); - return; - } + // Gather the metadata for this frame. + ImageFrame currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); + bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); - // Gather the metadata for this frame. - ImageFrame currentFrame = image.Frames[i]; - ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); - bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); + if (!useLocal && !hasPaletteQuantizer && i > 0) + { + // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. + // This allows a reduction of memory usage across multi-frame gifs using a global palette + // and also allows use to reuse the cache from previous runs. + int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1; + paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); + hasPaletteQuantizer = true; + } - if (!useLocal && !hasPaletteQuantizer && i > 0) - { - // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. - // This allows a reduction of memory usage across multi-frame gifs using a global palette - // and also allows use to reuse the cache from previous runs. - int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1; - paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); - hasPaletteQuantizer = true; + this.EncodeAdditionalFrame( + stream, + previousFrame, + currentFrame, + nextFrame, + encodingFrame, + useLocal, + gifMetadata, + paletteQuantizer, + previousDisposalMode); + + previousFrame = currentFrame; + previousDisposalMode = gifMetadata.DisposalMode; } - - this.EncodeAdditionalFrame( - stream, - previousFrame, - currentFrame, - nextFrame, - encodingFrame, - useLocal, - gifMetadata, - paletteQuantizer, - previousDisposalMode); - - previousFrame = currentFrame; - previousDisposalMode = gifMetadata.DisposalMode; } - - if (hasPaletteQuantizer) + finally { - paletteQuantizer.Dispose(); + if (hasPaletteQuantizer) + { + paletteQuantizer.Dispose(); + } } } @@ -324,7 +346,9 @@ private void EncodeAdditionalFrame( // We use it to determine the value to use to replace duplicate pixels. int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1; - ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground + ? null : + previousFrame; Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground ? this.backgroundColor ?? Color.Transparent @@ -341,6 +365,11 @@ private void EncodeAdditionalFrame( background, true); + if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode)) + { + EncodingUtilities.ClearTransparentPixels(encodingFrame, background); + } + using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( encodingFrame, bounds, diff --git a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs index 44431aa9a4..d2c3ad6907 100644 --- a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs +++ b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs @@ -30,7 +30,7 @@ public interface IAnimatedImageEncoder /// /// Acts as a base class for all image encoders that allow encoding animation sequences. /// -public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder +public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder { /// public Color? BackgroundColor { get; init; } diff --git a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs index e88b3ecf02..5edf6e40e9 100644 --- a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs +++ b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs @@ -24,7 +24,7 @@ public interface IQuantizingImageEncoder /// /// Acts as a base class for all image encoders that allow color palette generation via quantization. /// -public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder +public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder { /// public IQuantizer? Quantizer { get; init; } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 4b973d5115..80c3ec4c31 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -63,7 +63,6 @@ public void Encode( this.entries[i].Entry.ImageOffset = (uint)stream.Position; // We crop the frame to the size specified in the metadata. - // TODO: we can optimize this by cropping the frame only if the new size is both required and different. using Image encodingFrame = new(width, height); for (int y = 0; y < height; y++) { @@ -82,6 +81,8 @@ public void Encode( UseDoubleHeight = true, SkipFileHeader = true, SupportTransparency = false, + TransparentColorMode = this.encoder.TransparentColorMode, + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, BitsPerPixel = encodingMetadata.BmpBitsPerPixel }, IconFrameCompression.Png => new PngEncoder() @@ -90,6 +91,7 @@ public void Encode( // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 BitDepth = PngBitDepth.Bit8, ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = this.encoder.TransparentColorMode, CompressionLevel = PngCompressionLevel.BestCompression }, _ => throw new NotSupportedException(), diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index d9f71e1b56..63e675b505 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -68,12 +68,6 @@ public PngEncoder() /// public PngChunkFilter? ChunkFilter { get; init; } - /// - /// 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. - /// - public PngTransparentColorMode TransparentColorMode { get; init; } - /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 398c80634c..05220e8019 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -188,18 +189,18 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame currentFrame = image.Frames.RootFrame; int currentFrameIndex = 0; - bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; + bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels(this.encoder.TransparentColorMode); if (clearTransparency) { currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame, Color.Transparent); + EncodingUtilities.ClearTransparentPixels(currentFrame, Color.Transparent); } // Do not move this. We require an accurate bit depth for the header chunk. IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth( pngMetadata, currentFrame, - currentFrame.Bounds(), + currentFrame.Bounds, null); this.WriteHeaderChunk(stream); @@ -230,86 +231,88 @@ public void Encode(Image image, Stream stream, CancellationToken currentFrameIndex++; } - if (image.Frames.Count > 1) + try { - // Write the first animated frame. - currentFrame = image.Frames[currentFrameIndex]; - PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata(); - FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; - FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); - uint sequenceNumber = 1; - if (pngMetadata.AnimateRootFrame) + if (image.Frames.Count > 1) { - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); - } - else - { - sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true); - } - - currentFrameIndex++; + // Write the first animated frame. + currentFrame = image.Frames[currentFrameIndex]; + PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata(); + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; + FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0); + uint sequenceNumber = 1; + if (pngMetadata.AnimateRootFrame) + { + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + } + else + { + sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true); + } - // Capture the global palette for reuse on subsequent frames. - ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); + currentFrameIndex++; - // Write following frames. - ImageFrame previousFrame = image.Frames.RootFrame; + // Capture the global palette for reuse on subsequent frames. + ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); - // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); + // Write following frames. + ImageFrame previousFrame = image.Frames.RootFrame; - for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); - ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; - currentFrame = image.Frames[currentFrameIndex]; - ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; - - frameMetadata = currentFrame.Metadata.GetPngMetadata(); - bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; - Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground - ? this.backgroundColor ?? Color.Transparent - : Color.Transparent; - - (bool difference, Rectangle bounds) = - AnimationUtilities.DeDuplicatePixels( - image.Configuration, - prev, - currentFrame, - nextFrame, - encodingFrame, - background, - blend); - - if (clearTransparency) + for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { - ClearTransparentPixels(encodingFrame, background); - } + cancellationToken.ThrowIfCancellationRequested(); + + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + currentFrame = image.Frames[currentFrameIndex]; + ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; + + frameMetadata = currentFrame.Metadata.GetPngMetadata(); + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + background, + blend); + + if (clearTransparency) + { + EncodingUtilities.ClearTransparentPixels(encodingFrame, background); + } - // Each frame control sequence number must be incremented by the number of frame data chunks that follow. - frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); + // Each frame control sequence number must be incremented by the number of frame data chunks that follow. + frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); - // Dispose of previous quantized frame and reassign. - quantized?.Dispose(); - quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); - sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; + // Dispose of previous quantized frame and reassign. + quantized?.Dispose(); + quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); + sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; - previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMode; + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMode; + } } - } - this.WriteEndChunk(stream); + this.WriteEndChunk(stream); - stream.Flush(); - - // Dispose of allocations from final frame. - clonedFrame?.Dispose(); - quantized?.Dispose(); + stream.Flush(); + } + finally + { + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); + quantized?.Dispose(); + } } /// @@ -319,33 +322,6 @@ public void Dispose() this.currentScanline?.Dispose(); } - /// - /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. - /// - /// The type of the pixel. - /// The cloned image frame where the transparent pixels will be changed. - /// The color to replace transparent pixels with. - private static void ClearTransparentPixels(ImageFrame clone, Color color) - where TPixel : unmanaged, IPixel - => clone.ProcessPixelRows(accessor => - { - // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 transparent = color.ToPixel(); - for (int y = 0; y < accessor.Height; y++) - { - Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) - { - ref TPixel pixel = ref span[x]; - Rgba32 rgba = pixel.ToRgba32(); - if (rgba.A is 0) - { - pixel = TPixel.FromRgba32(transparent); - } - } - } - }); - /// /// Creates the quantized image and calculates and sets the bit depth. /// diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs deleted file mode 100644 index 76a89608bd..0000000000 --- a/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Png; - -/// -/// Enum indicating how the transparency should be handled on encoding. -/// -public enum PngTransparentColorMode -{ - /// - /// The transparency will be kept as is. - /// - Preserve = 0, - - /// - /// Converts fully transparent pixels that may contain R, G, B values which are not 0, - /// to transparent black, which can yield in better compression in some cases. - /// - Clear = 1, -} diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs index b9c2078b3f..1da9caffb5 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi; /// /// Image encoder for writing an image to a stream as a QOI image /// -public class QoiEncoder : ImageEncoder +public class QoiEncoder : AlphaAwareImageEncoder { /// /// Gets the color channels on the image that can be diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs index 88d87a3825..872cec3fd0 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -55,7 +55,7 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(stream, nameof(stream)); this.WriteHeader(image, stream); - this.WritePixels(image, stream); + this.WritePixels(image, stream, cancellationToken); WriteEndOfStream(stream); stream.Flush(); } @@ -78,7 +78,7 @@ private void WriteHeader(Image image, Stream stream) stream.WriteByte((byte)qoiColorSpace); } - private void WritePixels(Image image, Stream stream) + private void WritePixels(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // Start image encoding @@ -86,137 +86,156 @@ private void WritePixels(Image image, Stream stream) Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); Rgba32 previousPixel = new(0, 0, 0, 255); Rgba32 currentRgba32 = default; - Buffer2D pixels = image.Frames[0].PixelBuffer; - using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width); - Span rgbaRow = rgbaRowBuffer.GetSpan(); - for (int i = 0; i < pixels.Height; i++) + ImageFrame? clonedFrame = null; + try { - Span row = pixels.DangerousGetRowSpan(i); - PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow); - for (int j = 0; j < row.Length && i < pixels.Height; j++) + if (EncodingUtilities.ShouldClearTransparentPixels(this.encoder.TransparentColorMode)) { - // We get the RGBA value from pixels - currentRgba32 = rgbaRow[j]; + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + Buffer2D pixels = encodingFrame.PixelBuffer; + + using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width); + Span rgbaRow = rgbaRowBuffer.GetSpan(); + Configuration configuration = this.configuration; + for (int i = 0; i < pixels.Height; i++) + { + cancellationToken.ThrowIfCancellationRequested(); - // First, we check if the current pixel is equal to the previous one - // If so, we do a QOI_OP_RUN - if (currentRgba32.Equals(previousPixel)) + Span row = pixels.DangerousGetRowSpan(i); + PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow); + for (int j = 0; j < row.Length && i < pixels.Height; j++) { - /* It looks like this isn't an error, but this makes possible that - * files start with a QOI_OP_RUN if their first pixel is a fully opaque - * black. However, the decoder of this project takes that into consideration - * - * To further details, see https://github.com/phoboslab/qoi/issues/258, - * and we should discuss what to do about this approach and - * if it's correct - */ - int repetitions = 0; - do + // We get the RGBA value from pixels + currentRgba32 = rgbaRow[j]; + + // First, we check if the current pixel is equal to the previous one + // If so, we do a QOI_OP_RUN + if (currentRgba32.Equals(previousPixel)) { - repetitions++; - j++; - if (j == row.Length) + /* It looks like this isn't an error, but this makes possible that + * files start with a QOI_OP_RUN if their first pixel is a fully opaque + * black. However, the decoder of this project takes that into consideration + * + * To further details, see https://github.com/phoboslab/qoi/issues/258, + * and we should discuss what to do about this approach and + * if it's correct + */ + int repetitions = 0; + do { - j = 0; - i++; - if (i == pixels.Height) + repetitions++; + j++; + if (j == row.Length) { - break; + j = 0; + i++; + if (i == pixels.Height) + { + break; + } + + row = pixels.DangerousGetRowSpan(i); + PixelOperations.Instance.ToRgba32(configuration, row, rgbaRow); } - row = pixels.DangerousGetRowSpan(i); - PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow); + currentRgba32 = rgbaRow[j]; } + while (currentRgba32.Equals(previousPixel) && repetitions < 62); - currentRgba32 = rgbaRow[j]; - } - while (currentRgba32.Equals(previousPixel) && repetitions < 62); - - j--; - stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1))); + j--; + stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1))); - /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since - * it will be taken and compared on the next iteration - */ - continue; - } + /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since + * it will be taken and compared on the next iteration + */ + continue; + } - // else, we check if it exists in the previously seen pixels - // If so, we do a QOI_OP_INDEX - int pixelArrayPosition = GetArrayPosition(currentRgba32); - if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32)) - { - stream.WriteByte((byte)pixelArrayPosition); - } - else - { - // else, we check if the difference is less than -2..1 - // Since it wasn't found on the previously seen pixels, we save it - previouslySeenPixels[pixelArrayPosition] = currentRgba32; - - int diffRed = currentRgba32.R - previousPixel.R; - int diffGreen = currentRgba32.G - previousPixel.G; - int diffBlue = currentRgba32.B - previousPixel.B; - - // If so, we do a QOI_OP_DIFF - if (diffRed is >= -2 and <= 1 && - diffGreen is >= -2 and <= 1 && - diffBlue is >= -2 and <= 1 && - currentRgba32.A == previousPixel.A) + // else, we check if it exists in the previously seen pixels + // If so, we do a QOI_OP_INDEX + int pixelArrayPosition = GetArrayPosition(currentRgba32); + if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32)) { - // Bottom limit is -2, so we add 2 to make it equal to 0 - int dr = diffRed + 2; - int dg = diffGreen + 2; - int db = diffBlue + 2; - byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db); - stream.WriteByte(valueToWrite); + stream.WriteByte((byte)pixelArrayPosition); } else { - // else, we check if the green difference is less than -32..31 and the rest -8..7 - // If so, we do a QOI_OP_LUMA - int diffRedGreen = diffRed - diffGreen; - int diffBlueGreen = diffBlue - diffGreen; - if (diffGreen is >= -32 and <= 31 && - diffRedGreen is >= -8 and <= 7 && - diffBlueGreen is >= -8 and <= 7 && + // else, we check if the difference is less than -2..1 + // Since it wasn't found on the previously seen pixels, we save it + previouslySeenPixels[pixelArrayPosition] = currentRgba32; + + int diffRed = currentRgba32.R - previousPixel.R; + int diffGreen = currentRgba32.G - previousPixel.G; + int diffBlue = currentRgba32.B - previousPixel.B; + + // If so, we do a QOI_OP_DIFF + if (diffRed is >= -2 and <= 1 && + diffGreen is >= -2 and <= 1 && + diffBlue is >= -2 and <= 1 && currentRgba32.A == previousPixel.A) { - int dr_dg = diffRedGreen + 8; - int db_dg = diffBlueGreen + 8; - byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32)); - byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg); - stream.WriteByte(byteToWrite1); - stream.WriteByte(byteToWrite2); + // Bottom limit is -2, so we add 2 to make it equal to 0 + int dr = diffRed + 2; + int dg = diffGreen + 2; + int db = diffBlue + 2; + byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db); + stream.WriteByte(valueToWrite); } else { - // else, we check if the alpha is equal to the previous pixel - // If so, we do a QOI_OP_RGB - if (currentRgba32.A == previousPixel.A) + // else, we check if the green difference is less than -32..31 and the rest -8..7 + // If so, we do a QOI_OP_LUMA + int diffRedGreen = diffRed - diffGreen; + int diffBlueGreen = diffBlue - diffGreen; + if (diffGreen is >= -32 and <= 31 && + diffRedGreen is >= -8 and <= 7 && + diffBlueGreen is >= -8 and <= 7 && + currentRgba32.A == previousPixel.A) { - stream.WriteByte((byte)QoiChunk.QoiOpRgb); - stream.WriteByte(currentRgba32.R); - stream.WriteByte(currentRgba32.G); - stream.WriteByte(currentRgba32.B); + int dr_dg = diffRedGreen + 8; + int db_dg = diffBlueGreen + 8; + byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32)); + byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg); + stream.WriteByte(byteToWrite1); + stream.WriteByte(byteToWrite2); } else { - // else, we do a QOI_OP_RGBA - stream.WriteByte((byte)QoiChunk.QoiOpRgba); - stream.WriteByte(currentRgba32.R); - stream.WriteByte(currentRgba32.G); - stream.WriteByte(currentRgba32.B); - stream.WriteByte(currentRgba32.A); + // else, we check if the alpha is equal to the previous pixel + // If so, we do a QOI_OP_RGB + if (currentRgba32.A == previousPixel.A) + { + stream.WriteByte((byte)QoiChunk.QoiOpRgb); + stream.WriteByte(currentRgba32.R); + stream.WriteByte(currentRgba32.G); + stream.WriteByte(currentRgba32.B); + } + else + { + // else, we do a QOI_OP_RGBA + stream.WriteByte((byte)QoiChunk.QoiOpRgba); + stream.WriteByte(currentRgba32.R); + stream.WriteByte(currentRgba32.G); + stream.WriteByte(currentRgba32.B); + stream.WriteByte(currentRgba32.A); + } } } } - } - previousPixel = currentRgba32; + previousPixel = currentRgba32; + } } } + finally + { + clonedFrame?.Dispose(); + } } private static void WriteEndOfStream(Stream stream) diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 09b12e6081..a4630a464b 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// /// Image encoder for writing an image to a stream as a Targa true-vision image. /// -public sealed class TgaEncoder : ImageEncoder +public sealed class TgaEncoder : AlphaAwareImageEncoder { /// /// Gets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 1e05a9f716..e2ea9c4fe7 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -29,6 +29,8 @@ internal sealed class TgaEncoderCore /// private readonly TgaCompression compression; + private readonly TransparentColorMode transparentColorMode; + /// /// Initializes a new instance of the class. /// @@ -39,6 +41,7 @@ public TgaEncoderCore(TgaEncoder encoder, MemoryAllocator memoryAllocator) this.memoryAllocator = memoryAllocator; this.bitsPerPixel = encoder.BitsPerPixel; this.compression = encoder.Compression; + this.transparentColorMode = encoder.TransparentColorMode; } /// @@ -103,16 +106,33 @@ public void Encode(Image image, Stream stream, CancellationToken fileHeader.WriteTo(buffer); stream.Write(buffer, 0, TgaFileHeader.Size); - if (this.compression is TgaCompression.RunLength) + + ImageFrame? clonedFrame = null; + try { - this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame, cancellationToken); + if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode)) + { + clonedFrame = image.Frames.RootFrame.Clone(); + EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent); + } + + ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + + if (this.compression is TgaCompression.RunLength) + { + this.WriteRunLengthEncodedImage(stream, encodingFrame, cancellationToken); + } + else + { + this.WriteImage(image.Configuration, stream, encodingFrame, cancellationToken); + } + + stream.Flush(); } - else + finally { - this.WriteImage(image.Configuration, stream, image.Frames.RootFrame, cancellationToken); + clonedFrame?.Dispose(); } - - stream.Flush(); } /// diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index b560067f3f..4f6985f9db 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -49,6 +49,11 @@ internal sealed class TiffEncoderCore /// private readonly DeflateCompressionLevel compressionLevel; + /// + /// The transparent color mode to use when encoding. + /// + private readonly TransparentColorMode transparentColorMode; + /// /// Whether to skip metadata during encoding. /// @@ -59,20 +64,21 @@ internal sealed class TiffEncoderCore /// /// Initializes a new instance of the class. /// - /// The options for the encoder. + /// The options for the encoder. /// The global configuration. - public TiffEncoderCore(TiffEncoder options, Configuration configuration) + public TiffEncoderCore(TiffEncoder encoder, Configuration configuration) { this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; - this.PhotometricInterpretation = options.PhotometricInterpretation; - this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; - 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; + this.PhotometricInterpretation = encoder.PhotometricInterpretation; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; + this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.BitsPerPixel = encoder.BitsPerPixel; + this.HorizontalPredictor = encoder.HorizontalPredictor; + this.CompressionType = encoder.Compression; + this.compressionLevel = encoder.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; + this.skipMetadata = encoder.SkipMetadata; + this.transparentColorMode = encoder.TransparentColorMode; } /// @@ -135,10 +141,26 @@ public void Encode(Image image, Stream stream, CancellationToken foreach (ImageFrame frame in image.Frames) { - cancellationToken.ThrowIfCancellationRequested(); + ImageFrame? clonedFrame = null; + try + { + cancellationToken.ThrowIfCancellationRequested(); - ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); - metadataImage = null; + if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode)) + { + clonedFrame = frame.Clone(); + EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent); + } + + ImageFrame encodingFrame = clonedFrame ?? frame; + + ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); + metadataImage = null; + } + finally + { + clonedFrame?.Dispose(); + } } long currentOffset = writer.BaseStream.Position; diff --git a/src/ImageSharp/Formats/TransparentColorMode.cs b/src/ImageSharp/Formats/TransparentColorMode.cs new file mode 100644 index 0000000000..39986b5024 --- /dev/null +++ b/src/ImageSharp/Formats/TransparentColorMode.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Specifies how transparent pixels should be handled during encoding. +/// +public enum TransparentColorMode +{ + /// + /// Retains the original color values of transparent pixels. + /// + Preserve = 0, + + /// + /// Converts transparent pixels with non-zero color components + /// to fully transparent pixels (all components set to zero), + /// which may improve compression. + /// + Clear = 1, +} diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 46030dde32..fd6f508e4a 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -49,7 +49,7 @@ public static IMemoryOwner EncodeAlpha( quality, skipMetadata, effort, - WebpTransparentColorMode.Preserve, + TransparentColorMode.Preserve, false, 0); diff --git a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs index 2170eb1985..736070a1c9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs @@ -47,7 +47,7 @@ public static void ResidualImage( int[][] bestHisto, bool nearLossless, int nearLosslessQuality, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool lowEffort) { @@ -202,7 +202,7 @@ private static int GetBestPredictorForTile( int[][] histoArgb, int[][] bestHisto, int maxQuantization, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, Span modes, @@ -340,19 +340,20 @@ private static void GetResidual( int xEnd, int y, int maxQuantization, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, Span output, Span scratch) { - if (transparentColorMode == WebpTransparentColorMode.Preserve) + if (transparentColorMode == TransparentColorMode.Preserve) { PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch); } else { #pragma warning disable SA1503 // Braces should not be omitted +#pragma warning disable RCS1001 // Add braces (when expression spans over multiple lines) fixed (uint* currentRow = currentRowSpan) fixed (uint* upperRow = upperRowSpan) { @@ -466,6 +467,7 @@ private static void GetResidual( } } } +#pragma warning restore RCS1001 // Add braces (when expression spans over multiple lines) #pragma warning restore SA1503 // Braces should not be omitted /// @@ -577,7 +579,7 @@ private static void CopyImageWithPrediction( Span argbScratch, Span argb, int maxQuantization, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, bool lowEffort) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index e077249696..f088448391 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -69,7 +69,7 @@ internal class Vp8LEncoder : IDisposable /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible /// RGB information for better compression. /// - private readonly WebpTransparentColorMode transparentColorMode; + private readonly TransparentColorMode transparentColorMode; /// /// Whether to skip metadata during encoding. @@ -114,7 +114,7 @@ public Vp8LEncoder( uint quality, bool skipMetadata, WebpEncodingMethod method, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool nearLossless, int nearLosslessQuality) { diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index bfaaa831e1..b74337ef37 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -343,7 +343,7 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba return; } - Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value); + Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value); Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); TPixel backgroundPixel = backgroundColor.ToPixel(); pixelRegion.Fill(backgroundPixel); diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 226719c792..622fe0181e 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -58,13 +58,6 @@ public sealed class WebpEncoder : AnimatedImageEncoder /// public int FilterStrength { get; init; } = 60; - /// - /// 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; - /// /// 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. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 37d2ae0a19..b8f27a8326 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -54,7 +54,7 @@ internal sealed class WebpEncoderCore /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible /// RGB information for better compression. /// - private readonly WebpTransparentColorMode transparentColorMode; + private readonly TransparentColorMode transparentColorMode; /// /// Whether to skip metadata during encoding. @@ -166,7 +166,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); - hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation); + hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation); if (hasAnimation) { @@ -178,10 +178,7 @@ public void Encode(Image image, Stream stream, CancellationToken for (int i = 1; i < image.Frames.Count; i++) { - if (cancellationToken.IsCancellationRequested) - { - break; - } + cancellationToken.ThrowIfCancellationRequested(); ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; @@ -253,7 +250,7 @@ public void Encode(Image image, Stream stream, CancellationToken WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; - hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); + hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds, frameMetadata); // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. @@ -261,10 +258,7 @@ public void Encode(Image image, Stream stream, CancellationToken for (int i = 1; i < image.Frames.Count; i++) { - if (cancellationToken.IsCancellationRequested) - { - break; - } + cancellationToken.ThrowIfCancellationRequested(); ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; diff --git a/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs b/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs deleted file mode 100644 index c12b8ed976..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Enum indicating how the transparency should be handled on encoding. -/// -public enum WebpTransparentColorMode -{ - /// - /// Discard the transparency information for better compression. - /// - Clear = 0, - - /// - /// The transparency will be kept as is. - /// - Preserve = 1, -} diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index fdde5019e1..686292bc7f 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -56,7 +56,7 @@ protected ImageFrame(Configuration configuration, int width, int height, ImageFr /// Gets the bounds of the frame. /// /// The - public Rectangle Bounds() => new(0, 0, this.Width, this.Height); + public Rectangle Bounds => new(0, 0, this.Width, this.Height); /// public void Dispose() diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 2287f65cd8..de71e77ca0 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -429,7 +429,7 @@ internal ImageFrame CloneAs(Configuration configuration) ParallelRowIterator.IterateRowIntervals( configuration, - this.Bounds(), + this.Bounds, in operation); return target; diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 2eb05ea935..f0fa1438dd 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -25,27 +25,65 @@ public static IMemoryGroup GetMemoryGroup(this Buffer2D buffer) return buffer.FastMemoryGroup.View; } + /// + /// Performs a deep clone of the buffer covering the specified . + /// + /// The element type. + /// The source buffer. + /// The configuration. + /// The rectangle to clone. + /// The . + internal static Buffer2D CloneRegion(this Buffer2D source, Configuration configuration, Rectangle rectangle) + where T : unmanaged + { + Buffer2D buffer = configuration.MemoryAllocator.Allocate2D( + rectangle.Width, + rectangle.Height, + configuration.PreferContiguousImageBuffers); + + // Optimization for when the size of the area is the same as the buffer size. + Buffer2DRegion sourceRegion = source.GetRegion(rectangle); + if (sourceRegion.IsFullBufferArea) + { + sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup); + } + else + { + for (int y = 0; y < rectangle.Height; y++) + { + sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y)); + } + } + + return buffer; + } + /// /// TODO: Does not work with multi-buffer groups, should be specific to Resize. - /// Copy columns of inplace, - /// from positions starting at to positions at . + /// Copy columns of in-place, + /// from positions starting at to positions at . /// + /// The element type. + /// The . + /// The source column index. + /// The destination column index. + /// The number of columns to copy. internal static unsafe void DangerousCopyColumns( this Buffer2D buffer, int sourceIndex, - int destIndex, + int destinationIndex, int columnCount) where T : struct { DebugGuard.NotNull(buffer, nameof(buffer)); DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex)); - DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex)); - CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount); + DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex)); + CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount); int elementSize = Unsafe.SizeOf(); int width = buffer.Width * elementSize; int sOffset = sourceIndex * elementSize; - int dOffset = destIndex * elementSize; + int dOffset = destinationIndex * elementSize; long count = columnCount * elementSize; Span span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span); @@ -73,9 +111,7 @@ internal static unsafe void DangerousCopyColumns( /// The internal static Rectangle FullRectangle(this Buffer2D buffer) where T : struct - { - return new Rectangle(0, 0, buffer.Width, buffer.Height); - } + => new(0, 0, buffer.Width, buffer.Height); /// /// Return a to the subregion represented by 'rectangle' @@ -86,11 +122,11 @@ internal static Rectangle FullRectangle(this Buffer2D buffer) /// The internal static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) where T : unmanaged => - new Buffer2DRegion(buffer, rectangle); + new(buffer, rectangle); internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) where T : unmanaged => - new Buffer2DRegion(buffer, new Rectangle(x, y, width, height)); + new(buffer, new Rectangle(x, y, width, height)); /// /// Return a to the whole area of 'buffer' @@ -100,7 +136,7 @@ internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, i /// The internal static Buffer2DRegion GetRegion(this Buffer2D buffer) where T : unmanaged => - new Buffer2DRegion(buffer); + new(buffer); /// /// Returns the size of the buffer. @@ -115,6 +151,8 @@ internal static Size Size(this Buffer2D buffer) /// /// Gets the bounds of the buffer. /// + /// The element type + /// The /// The internal static Rectangle Bounds(this Buffer2D buffer) where T : struct => diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index 033b0a25a6..f4b257b587 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -107,7 +107,7 @@ public Span DangerousGetRowSpan(int y) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Buffer2DRegion GetSubRegion(int x, int y, int width, int height) { - var rectangle = new Rectangle(x, y, width, height); + Rectangle rectangle = new(x, y, width, height); return this.GetSubRegion(rectangle); } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs index b4b1ffc6f4..e2e933f3cc 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -36,8 +36,13 @@ internal static void Clear(this IMemoryGroup group) /// /// Returns a slice that is expected to be within the bounds of a single buffer. - /// Otherwise is thrown. /// + /// The type of element. + /// The group. + /// The start index of the slice. + /// The length of the slice. + /// Slice is out of bounds. + /// The slice. internal static Memory GetBoundedMemorySlice(this IMemoryGroup group, long start, int length) where T : struct { diff --git a/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs b/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs index 4d95e060dc..63c4895080 100644 --- a/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs +++ b/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs @@ -61,9 +61,7 @@ public Image GetResultImage() /// public IImageProcessingContext ApplyProcessor(IImageProcessor processor) - { - return this.ApplyProcessor(processor, this.GetCurrentBounds()); - } + => this.ApplyProcessor(processor, this.GetCurrentBounds()); /// public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle) @@ -74,11 +72,9 @@ public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectang // interim clone if the first processor in the pipeline is a cloning processor. if (processor is ICloningImageProcessor cloningImageProcessor) { - using (ICloningImageProcessor pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle)) - { - this.destination = pixelProcessor.CloneAndExecute(); - return this; - } + using ICloningImageProcessor pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle); + this.destination = pixelProcessor.CloneAndExecute(); + return this; } // Not a cloning processor? We need to create a clone to operate on. diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs index 713d4d5b77..676acee0f4 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -42,7 +41,7 @@ public static Buffer2D CalculateIntegralImage(this Image /// The containing all the sums. public static Buffer2D CalculateIntegralImage(this ImageFrame source) where TPixel : unmanaged, IPixel - => source.CalculateIntegralImage(source.Bounds()); + => source.CalculateIntegralImage(source.Bounds); /// /// Apply an image integral. @@ -56,7 +55,7 @@ public static Buffer2D CalculateIntegralImage(this ImageFrame /// Performs Bradley Adaptive Threshold filter against an image. /// +/// The pixel format. internal class AdaptiveThresholdProcessor : ImageProcessor where TPixel : unmanaged, IPixel { @@ -30,7 +31,7 @@ public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThreshold /// protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); Configuration configuration = this.Configuration; TPixel upper = this.definition.Upper.ToPixel(); @@ -97,19 +98,23 @@ public void Invoke(int y, Span span) Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); PixelOperations.Instance.ToL8(this.configuration, rowSpan, span); + int startY = this.startY; int maxX = this.bounds.Width - 1; int maxY = this.bounds.Height - 1; + int clusterSize = this.clusterSize; + float thresholdLimit = this.thresholdLimit; + Buffer2D image = this.intImage; for (int x = 0; x < rowSpan.Length; x++) { - int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX); - int x2 = Math.Min(x + this.clusterSize + 1, maxX); - int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY); - int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY); + int x1 = Math.Clamp(x - clusterSize + 1, 0, maxX); + int x2 = Math.Min(x + clusterSize + 1, maxX); + int y1 = Math.Clamp(y - startY - clusterSize + 1, 0, maxY); + int y2 = Math.Min(y - startY + clusterSize + 1, maxY); uint count = (uint)((x2 - x1) * (y2 - y1)); - ulong sum = Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], ulong.MaxValue); + ulong sum = Math.Min(image[x2, y2] - image[x1, y2] - image[x2, y1] + image[x1, y1], ulong.MaxValue); - if (span[x].PackedValue * count <= sum * this.thresholdLimit) + if (span[x].PackedValue * count <= sum * thresholdLimit) { rowSpan[x] = this.lower; } diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs index 1c76ea6a45..ad87f36c1c 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -38,8 +38,8 @@ protected override void OnFrameApply(ImageFrame source) Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - var operation = new RowOperation( + Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds); + RowOperation operation = new( interest.X, source.PixelBuffer, upper, @@ -169,10 +169,8 @@ private static float GetSaturation(Rgb24 rgb) { return chroma / (max + min); } - else - { - return chroma / (2F - max - min); - } + + return chroma / (2F - max - min); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index 5931b7c402..a96fa1993e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -75,12 +75,12 @@ public BokehBlurProcessor(Configuration configuration, BokehBlurProcessor defini /// protected override void OnFrameApply(ImageFrame source) { - var sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds); // Preliminary gamma highlight pass if (this.gamma == 3F) { - var gammaOperation = new ApplyGamma3ExposureRowOperation(sourceRectangle, source.PixelBuffer, this.Configuration); + ApplyGamma3ExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration); ParallelRowIterator.IterateRows( this.Configuration, sourceRectangle, @@ -88,7 +88,7 @@ protected override void OnFrameApply(ImageFrame source) } else { - var gammaOperation = new ApplyGammaExposureRowOperation(sourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); + ApplyGammaExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); ParallelRowIterator.IterateRows( this.Configuration, sourceRectangle, @@ -104,7 +104,7 @@ protected override void OnFrameApply(ImageFrame source) // Apply the inverse gamma exposure pass, and write the final pixel data if (this.gamma == 3F) { - var operation = new ApplyInverseGamma3ExposureRowOperation(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration); + ApplyInverseGamma3ExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration); ParallelRowIterator.IterateRows( this.Configuration, sourceRectangle, @@ -112,7 +112,7 @@ protected override void OnFrameApply(ImageFrame source) } else { - var operation = new ApplyInverseGammaExposureRowOperation(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, 1 / this.gamma); + ApplyInverseGammaExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, 1 / this.gamma); ParallelRowIterator.IterateRows( this.Configuration, sourceRectangle, @@ -146,7 +146,7 @@ private void OnFrameApplyCore( // doing two 1D convolutions with the same kernel, we can use a single kernel sampling map as if // we were using a 2D kernel with each dimension being the same as the length of our kernel, and // use the two sampling offset spans resulting from this same map. This saves some extra work. - using var mapXY = new KernelSamplingMap(configuration.MemoryAllocator); + using KernelSamplingMap mapXY = new(configuration.MemoryAllocator); mapXY.BuildSamplingOffsetMap(this.kernelSize, this.kernelSize, sourceRectangle); @@ -161,7 +161,7 @@ private void OnFrameApplyCore( Vector4 parameters = Unsafe.Add(ref paramsRef, (uint)i); // Horizontal convolution - var horizontalOperation = new FirstPassConvolutionRowOperation( + FirstPassConvolutionRowOperation horizontalOperation = new( sourceRectangle, firstPassBuffer, source.PixelBuffer, @@ -175,7 +175,7 @@ private void OnFrameApplyCore( in horizontalOperation); // Vertical 1D convolutions to accumulate the partial results on the target buffer - var verticalOperation = new BokehBlurProcessor.SecondPassConvolutionRowOperation( + BokehBlurProcessor.SecondPassConvolutionRowOperation verticalOperation = new( sourceRectangle, processingBuffer, firstPassBuffer, @@ -342,9 +342,7 @@ public ApplyGamma3ExposureRowOperation( /// [MethodImpl(InliningOptions.ShortMethod)] public int GetRequiredBufferLength(Rectangle bounds) - { - return bounds.Width; - } + => bounds.Width; /// [MethodImpl(InliningOptions.ShortMethod)] @@ -391,7 +389,7 @@ public ApplyInverseGammaExposureRowOperation( public void Invoke(int y) { Vector4 low = Vector4.Zero; - var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + Vector4 high = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y)[this.bounds.X..]; diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs index 8f5ddd1690..02e06db494 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -62,7 +62,7 @@ protected override void OnFrameApply(ImageFrame source) source.CopyTo(targetPixels); - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); using (KernelSamplingMap map = new(allocator)) { diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index cdd8ff8ae6..1bbbdb3501 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -98,7 +98,7 @@ protected override void OnFrameApply(ImageFrame source) { using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); // We can create a single sampling map with the size as if we were using the non separated 2D kernel // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index 9b4659929b..feaaf30ce0 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -89,7 +89,7 @@ protected override void OnFrameApply(ImageFrame source) source.CopyTo(targetPixels); - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); using (KernelSamplingMap map = new(allocator)) { diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index a1fa4db973..eae7481661 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -58,7 +58,7 @@ protected override void BeforeImageApply() /// protected override void OnFrameApply(ImageFrame source) { - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); // We need a clean copy for each pass to start from using ImageFrame cleanCopy = source.Clone(); diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index fe3a29d437..3e75d8b840 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -29,7 +29,7 @@ protected override void OnFrameApply(ImageFrame source) source.CopyTo(targetPixels); - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); using KernelSamplingMap map = new(this.Configuration.MemoryAllocator); map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 982cc7d46c..7e672393c7 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -46,7 +46,7 @@ public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcesso /// protected override void OnFrameApply(ImageFrame source) { - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest); } diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs index 847a211a5a..06cfa49b3d 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs @@ -36,14 +36,12 @@ public PixelRowDelegateProcessor(PixelRowOperation pixelRowOperation, PixelConve /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - { - return new PixelRowDelegateProcessor( + => new PixelRowDelegateProcessor( new PixelRowDelegate(this.PixelRowOperation), configuration, this.Modifiers, source, sourceRectangle); - } /// /// A implementing the row processing logic for . @@ -54,9 +52,7 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio [MethodImpl(InliningOptions.ShortMethod)] public PixelRowDelegate(PixelRowOperation pixelRowOperation) - { - this.pixelRowOperation = pixelRowOperation; - } + => this.pixelRowOperation = pixelRowOperation; /// [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs index 36bb327cf2..d38ffc801e 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs @@ -48,8 +48,8 @@ public PixelRowDelegateProcessor( /// protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + RowOperation operation = new(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate); ParallelRowIterator.IterateRows( this.Configuration, diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs index 68000ba3e8..c828b95b62 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs @@ -32,7 +32,7 @@ public PixelateProcessor(Configuration configuration, PixelateProcessor definiti /// protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); int size = this.Size; Guard.MustBeBetweenOrEqualTo(size, 0, interest.Width, nameof(size)); diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs index 5109139647..37286086c6 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs @@ -27,15 +27,13 @@ internal class FilterProcessor : ImageProcessor /// The source area to process for the current processor instance. public FilterProcessor(Configuration configuration, FilterProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) - { - this.definition = definition; - } + => this.definition = definition; /// protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + RowOperation operation = new(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration); ParallelRowIterator.IterateRows( this.Configuration, diff --git a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs index 93e600106a..41560d1200 100644 --- a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs @@ -23,9 +23,9 @@ public OpaqueProcessor( protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); - var operation = new OpaqueRowOperation(this.Configuration, source.PixelBuffer, interest); + OpaqueRowOperation operation = new(this.Configuration, source.PixelBuffer, interest); ParallelRowIterator.IterateRows(this.Configuration, interest, in operation); } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index 6f4493f951..606789af96 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -28,9 +28,9 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor /// Indicating whether to clip the histogram bins at a specific value. /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Whether to apply a synchronized luminance value to each color channel. /// The source for the current processor instance. /// The source area to process for the current processor instance. - /// Whether to apply a synchronized luminance value to each color channel. public AutoLevelProcessor( Configuration configuration, int luminanceLevels, @@ -40,9 +40,7 @@ public AutoLevelProcessor( Image source, Rectangle sourceRectangle) : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) - { - this.SyncChannels = syncChannels; - } + => this.SyncChannels = syncChannels; /// /// Gets a value indicating whether to apply a synchronized luminance value to each color channel. @@ -54,12 +52,12 @@ protected override void OnFrameApply(ImageFrame source) { MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; int numberOfPixels = source.Width * source.Height; - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); // Build the histogram of the grayscale levels. - var grayscaleOperation = new GrayscaleLevelsRowOperation(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + GrayscaleLevelsRowOperation grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); ParallelRowIterator.IterateRows, Vector4>( this.Configuration, interest, @@ -83,7 +81,7 @@ ref MemoryMarshal.GetReference(histogram), if (this.SyncChannels) { - var cdfOperation = new SynchronizedChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + SynchronizedChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -91,7 +89,7 @@ ref MemoryMarshal.GetReference(histogram), } else { - var cdfOperation = new SeperateChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + SeperateChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -136,10 +134,10 @@ public SynchronizedChannelsRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span vectorBuffer = span.Slice(0, this.bounds.Width); + Span vectorBuffer = span[..this.bounds.Width]; ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); - var sourceAccess = new PixelAccessor(this.source); + PixelAccessor sourceAccess = new(this.source); int levels = this.luminanceLevels; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; @@ -148,12 +146,11 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { - var vector = Unsafe.Add(ref vectorRef, (uint)x); + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; float scalingFactor = scaledLuminance * levels / luminance; - Vector4 scaledVector = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); - Unsafe.Add(ref vectorRef, (uint)x) = scaledVector; + Unsafe.Add(ref vectorRef, (uint)x) = new(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); } PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); @@ -197,10 +194,10 @@ public SeperateChannelsRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span vectorBuffer = span.Slice(0, this.bounds.Width); + Span vectorBuffer = span[..this.bounds.Width]; ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); - var sourceAccess = new PixelAccessor(this.source); + PixelAccessor sourceAccess = new(this.source); int levelsMinusOne = this.luminanceLevels - 1; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; @@ -209,7 +206,7 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { - var vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne; + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne; uint originalX = (uint)MathF.Round(vector.X); float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index e7433899bf..3ab8f7431e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -46,12 +46,12 @@ protected override void OnFrameApply(ImageFrame source) { MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; int numberOfPixels = source.Width * source.Height; - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); // Build the histogram of the grayscale levels. - var grayscaleOperation = new GrayscaleLevelsRowOperation(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + GrayscaleLevelsRowOperation grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); ParallelRowIterator.IterateRows, Vector4>( this.Configuration, interest, @@ -74,7 +74,7 @@ ref MemoryMarshal.GetReference(histogram), float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; // Apply the cdf to each pixel of the image - var cdfOperation = new CdfApplicationRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + CdfApplicationRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -118,7 +118,7 @@ public CdfApplicationRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span vectorBuffer = span.Slice(0, this.bounds.Width); + Span vectorBuffer = span[..this.bounds.Width]; ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer); ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); int levels = this.luminanceLevels; @@ -129,7 +129,7 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { - var vector = Unsafe.Add(ref vectorRef, (uint)x); + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W); diff --git a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs index 1e43458253..9ed314ee38 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs @@ -35,7 +35,7 @@ protected override void OnFrameApply(ImageFrame source) TPixel color = this.definition.Color.ToPixel(); GraphicsOptions graphicsOptions = this.definition.GraphicsOptions; - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); Configuration configuration = this.Configuration; MemoryAllocator memoryAllocator = configuration.MemoryAllocator; @@ -48,7 +48,7 @@ protected override void OnFrameApply(ImageFrame source) PixelBlender blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); - var operation = new RowOperation(configuration, interest, blender, amount, colors, source.PixelBuffer); + RowOperation operation = new(configuration, interest, blender, amount, colors, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs index 19ce6c417d..d73e7bea1c 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs @@ -40,7 +40,7 @@ protected override void OnFrameApply(ImageFrame source) TPixel glowColor = this.definition.GlowColor.ToPixel(); float blendPercent = this.definition.GraphicsOptions.BlendPercentage; - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); Vector2 center = Rectangle.Center(interest); float finalRadius = this.definition.Radius.Calculate(interest.Size); @@ -54,7 +54,7 @@ protected override void OnFrameApply(ImageFrame source) using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(glowColor); - var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); + RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs index a327deec1c..b08cd898e8 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs @@ -40,7 +40,7 @@ protected override void OnFrameApply(ImageFrame source) TPixel vignetteColor = this.definition.VignetteColor.ToPixel(); float blendPercent = this.definition.GraphicsOptions.BlendPercentage; - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); Vector2 center = Rectangle.Center(interest); float finalRadiusX = this.definition.RadiusX.Calculate(interest.Size); @@ -62,7 +62,7 @@ protected override void OnFrameApply(ImageFrame source) using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(vignetteColor); - var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); + RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index da2580fedf..16bb412c76 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -32,7 +32,7 @@ public QuantizeProcessor(Configuration configuration, IQuantizer quantizer, Imag /// protected override void OnFrameApply(ImageFrame source) { - Rectangle interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle); + Rectangle interest = Rectangle.Intersect(source.Bounds, this.SourceRectangle); Configuration configuration = this.Configuration; using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration); diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs index fc1dda6be5..6d2200b8a7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -50,7 +51,7 @@ public static IndexedImageFrame BuildPaletteAndQuantizeFrame( Guard.NotNull(quantizer, nameof(quantizer)); Guard.NotNull(source, nameof(source)); - Rectangle 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,7 +78,7 @@ public static IndexedImageFrame QuantizeFrame( where TPixel : unmanaged, IPixel { Guard.NotNull(source, nameof(source)); - Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds); + Rectangle interest = Rectangle.Intersect(source.Bounds, bounds); IndexedImageFrame destination = new( quantizer.Configuration, @@ -111,10 +112,39 @@ public static void BuildPalette( IPixelSamplingStrategy pixelSamplingStrategy, Image source) where TPixel : unmanaged, IPixel + => quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source); + + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The configuration. + /// The transparent color mode. + /// The pixel sampling strategy. + /// The source image to sample from. + public static void BuildPalette( + this IQuantizer quantizer, + Configuration configuration, + TransparentColorMode mode, + IPixelSamplingStrategy pixelSamplingStrategy, + Image source) + where TPixel : unmanaged, IPixel { - foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + if (EncodingUtilities.ShouldClearTransparentPixels(mode)) + { + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + using Buffer2D clone = region.Buffer.CloneRegion(configuration, region.Rectangle); + quantizer.AddPaletteColors(clone.GetRegion()); + } + } + else { - quantizer.AddPaletteColors(region); + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + quantizer.AddPaletteColors(region); + } } } @@ -130,10 +160,39 @@ public static void BuildPalette( IPixelSamplingStrategy pixelSamplingStrategy, ImageFrame source) where TPixel : unmanaged, IPixel + => quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source); + + /// + /// Adds colors to the quantized palette from the given pixel regions. + /// + /// The pixel format. + /// The pixel specific quantizer. + /// The configuration. + /// The transparent color mode. + /// The pixel sampling strategy. + /// The source image frame to sample from. + public static void BuildPalette( + this IQuantizer quantizer, + Configuration configuration, + TransparentColorMode mode, + IPixelSamplingStrategy pixelSamplingStrategy, + ImageFrame source) + where TPixel : unmanaged, IPixel { - foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + if (EncodingUtilities.ShouldClearTransparentPixels(mode)) + { + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + using Buffer2D clone = region.Buffer.CloneRegion(configuration, region.Rectangle); + quantizer.AddPaletteColors(clone.GetRegion()); + } + } + else { - quantizer.AddPaletteColors(region); + foreach (Buffer2DRegion region in pixelSamplingStrategy.EnumeratePixelRegions(source)) + { + quantizer.AddPaletteColors(region); + } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index 888d513206..b3919e5844 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -61,7 +61,7 @@ public void ApplyTransform(in TResampler sampler) if (matrix.Equals(Matrix3x2.Identity)) { // The clone will be blank here copy all the pixel data over - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds); Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); for (int y = 0; y < sourceBuffer.Height; y++) @@ -79,13 +79,13 @@ public void ApplyTransform(in TResampler sampler) { NNAffineOperation nnOperation = new( source.PixelBuffer, - Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + Rectangle.Intersect(this.SourceRectangle, source.Bounds), destination.PixelBuffer, matrix); ParallelRowIterator.IterateRows( configuration, - destination.Bounds(), + destination.Bounds, in nnOperation); return; @@ -94,14 +94,14 @@ public void ApplyTransform(in TResampler sampler) AffineOperation operation = new( configuration, source.PixelBuffer, - Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + Rectangle.Intersect(this.SourceRectangle, source.Bounds), destination.PixelBuffer, in sampler, matrix); ParallelRowIterator.IterateRowIntervals, Vector4>( configuration, - destination.Bounds(), + destination.Bounds, in operation); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs index 14da3ac890..1adda1d043 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs @@ -72,10 +72,10 @@ private static void FlipX(Buffer2D source, Configuration configuration) /// The configuration. private static void FlipY(ImageFrame source, Configuration configuration) { - var operation = new RowOperation(source.PixelBuffer); + RowOperation operation = new(source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, - source.Bounds(), + source.Bounds, in operation); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index 068f69cebc..16b0070e90 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -61,7 +61,7 @@ public void ApplyTransform(in TResampler sampler) if (matrix.Equals(Matrix4x4.Identity)) { // The clone will be blank here copy all the pixel data over - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds); Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); for (int y = 0; y < sourceBuffer.Height; y++) @@ -79,13 +79,13 @@ public void ApplyTransform(in TResampler sampler) { NNProjectiveOperation nnOperation = new( source.PixelBuffer, - Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + Rectangle.Intersect(this.SourceRectangle, source.Bounds), destination.PixelBuffer, matrix); ParallelRowIterator.IterateRows( configuration, - destination.Bounds(), + destination.Bounds, in nnOperation); return; @@ -94,14 +94,14 @@ public void ApplyTransform(in TResampler sampler) ProjectiveOperation operation = new( configuration, source.PixelBuffer, - Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + Rectangle.Intersect(this.SourceRectangle, source.Bounds), destination.PixelBuffer, in sampler, matrix); ParallelRowIterator.IterateRowIntervals, Vector4>( configuration, - destination.Bounds(), + destination.Bounds, in operation); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs index 3ffb1ab518..8d3ded09f9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs @@ -130,40 +130,40 @@ private bool OptimizedApply( /// The configuration. private static void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate180RowOperation(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + Rotate180RowOperation operation = new(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRows( configuration, - source.Bounds(), + source.Bounds, in operation); } /// - /// Rotates the image 270 degrees clockwise at the centre point. + /// Rotates the image 270 degrees clockwise at the center point. /// /// The source image. /// The destination image. /// The configuration. private static void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + Rotate270RowIntervalOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRowIntervals( configuration, - source.Bounds(), + source.Bounds, in operation); } /// - /// Rotates the image 90 degrees clockwise at the centre point. + /// Rotates the image 90 degrees clockwise at the center point. /// /// The source image. /// The destination image. /// The configuration. private static void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate90RowOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); + Rotate90RowOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRows( configuration, - source.Bounds(), + source.Bounds, in operation); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs index d78640549d..31b6cbdde2 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Attributes; using ImageMagick; using ImageMagick.Formats; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests; @@ -102,7 +103,7 @@ public void ImageSharpWebpLossless() Quality = 75, // This is equal to exact = false in libwebp, which is the default. - TransparentColorMode = WebpTransparentColorMode.Clear + TransparentColorMode = TransparentColorMode.Clear }); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 53a795c649..b6bb243afa 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -346,7 +346,7 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color Image image = new(50, 50); PngEncoder encoder = new() { - TransparentColorMode = PngTransparentColorMode.Clear, + TransparentColorMode = TransparentColorMode.Clear, ColorType = colorType }; Rgba32 rgba32 = Color.Blue.ToPixel(); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 0aa56e252e..f82fa65df8 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -311,7 +311,7 @@ public void Encode_Lossless_WithPreserveTransparentColor_Works(TestImage { FileFormat = WebpFileFormatType.Lossless, Method = method, - TransparentColorMode = WebpTransparentColorMode.Preserve + TransparentColorMode = TransparentColorMode.Preserve }; using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs index 403865e662..5e5887c923 100644 --- a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 59a0ad4274..b59542482b 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -13,10 +13,10 @@ public class QuantizedImageTests [Fact] public void QuantizersDitherByDefault() { - var werner = new WernerPaletteQuantizer(); - var webSafe = new WebSafePaletteQuantizer(); - var octree = new OctreeQuantizer(); - var wu = new WuQuantizer(); + WernerPaletteQuantizer werner = new(); + WebSafePaletteQuantizer webSafe = new(); + OctreeQuantizer octree = new(); + WuQuantizer wu = new(); Assert.NotNull(werner.Options.Dither); Assert.NotNull(webSafe.Options.Dither); @@ -52,27 +52,23 @@ public void OctreeQuantizerYieldsCorrectTransparentPixel( bool dither) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - Assert.True(image[0, 0].Equals(default)); + using Image image = provider.GetImage(); + Assert.True(image[0, 0].Equals(default)); - var options = new QuantizerOptions(); - if (!dither) - { - options.Dither = null; - } + QuantizerOptions options = new(); + if (!dither) + { + options.Dither = null; + } - var quantizer = new OctreeQuantizer(options); + OctreeQuantizer quantizer = new(options); - foreach (ImageFrame frame in image.Frames) - { - using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration)) - using (IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) - { - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); - } - } + foreach (ImageFrame frame in image.Frames) + { + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); } } @@ -82,27 +78,23 @@ public void OctreeQuantizerYieldsCorrectTransparentPixel( public void WuQuantizerYieldsCorrectTransparentPixel(TestImageProvider provider, bool dither) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - Assert.True(image[0, 0].Equals(default)); + using Image image = provider.GetImage(); + Assert.True(image[0, 0].Equals(default)); - var options = new QuantizerOptions(); - if (!dither) - { - options.Dither = null; - } + QuantizerOptions options = new(); + if (!dither) + { + options.Dither = null; + } - var quantizer = new WuQuantizer(options); + WuQuantizer quantizer = new(options); - foreach (ImageFrame frame in image.Frames) - { - using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration)) - using (IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) - { - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); - } - } + foreach (ImageFrame frame in image.Frames) + { + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); } } @@ -112,13 +104,11 @@ public void WuQuantizerYieldsCorrectTransparentPixel(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - var octreeQuantizer = new OctreeQuantizer(); - IQuantizer quantizer = octreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions() { MaxColors = 128 }); - ImageFrame frame = image.Frames[0]; - quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); - } + using Image image = provider.GetImage(); + OctreeQuantizer octreeQuantizer = new(); + IQuantizer quantizer = octreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions() { MaxColors = 128 }); + ImageFrame frame = image.Frames[0]; + quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); } private int GetTransparentIndex(IndexedImageFrame quantized) diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index ccb79debda..74f2fc3b42 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -12,13 +12,13 @@ public class WuQuantizerTests public void SinglePixelOpaque() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + WuQuantizer quantizer = new(new QuantizerOptions { Dither = null }); - using var image = new Image(config, 1, 1, Color.Black.ToPixel()); + using Image image = new(config, 1, 1, Color.Black.ToPixel()); ImageFrame frame = image.Frames.RootFrame; using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); - using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Width); @@ -32,13 +32,13 @@ public void SinglePixelOpaque() public void SinglePixelTransparent() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + WuQuantizer quantizer = new(new QuantizerOptions { Dither = null }); - using var image = new Image(config, 1, 1, default(Rgba32)); + using Image image = new(config, 1, 1, default(Rgba32)); ImageFrame frame = image.Frames.RootFrame; using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); - using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Width); @@ -66,7 +66,7 @@ public void SinglePixelTransparent() [Fact] public void Palette256() { - using var image = new Image(1, 256); + using Image image = new(1, 256); for (int i = 0; i < 256; i++) { @@ -79,18 +79,18 @@ public void Palette256() } Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + WuQuantizer quantizer = new(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); - using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); Assert.Equal(256, result.Palette.Length); Assert.Equal(1, result.Width); Assert.Equal(256, result.Height); - using var actualImage = new Image(1, 256); + using Image actualImage = new(1, 256); actualImage.ProcessPixelRows(accessor => { @@ -123,72 +123,68 @@ public void LowVariance(TestImageProvider provider) where TPixel : unmanaged, IPixel { // See https://github.com/SixLabors/ImageSharp/issues/866 - using (Image image = provider.GetImage()) - { - Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); - ImageFrame frame = image.Frames.RootFrame; + using Image image = provider.GetImage(); + Configuration config = Configuration.Default; + WuQuantizer quantizer = new(new QuantizerOptions { Dither = null }); + ImageFrame frame = image.Frames.RootFrame; - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); - using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); - Assert.Equal(48, result.Palette.Length); - } + Assert.Equal(48, result.Palette.Length); } private static void TestScale(Func pixelBuilder) { - using (var image = new Image(1, 256)) - using (var expectedImage = new Image(1, 256)) - using (var actualImage = new Image(1, 256)) + using Image image = new(1, 256); + using Image expectedImage = new(1, 256); + using Image actualImage = new(1, 256); + for (int i = 0; i < 256; i++) { - for (int i = 0; i < 256; i++) - { - byte c = (byte)i; - image[0, i] = pixelBuilder.Invoke(c); - } + byte c = (byte)i; + image[0, i] = pixelBuilder.Invoke(c); + } - for (int i = 0; i < 256; i++) - { - byte c = (byte)((i & ~7) + 4); - expectedImage[0, i] = pixelBuilder.Invoke(c); - } + for (int i = 0; i < 256; i++) + { + byte c = (byte)((i & ~7) + 4); + expectedImage[0, i] = pixelBuilder.Invoke(c); + } - Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + Configuration config = Configuration.Default; + WuQuantizer quantizer = new(new QuantizerOptions { Dither = null }); - ImageFrame frame = image.Frames.RootFrame; - using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config)) - using (IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) - { - Assert.Equal(4 * 8, result.Palette.Length); - Assert.Equal(1, result.Width); - Assert.Equal(256, result.Height); + ImageFrame frame = image.Frames.RootFrame; + using (IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(config)) + using (IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds)) + { + Assert.Equal(4 * 8, result.Palette.Length); + Assert.Equal(1, result.Width); + Assert.Equal(256, result.Height); - actualImage.ProcessPixelRows(accessor => + actualImage.ProcessPixelRows(accessor => + { + ReadOnlySpan paletteSpan = result.Palette.Span; + int paletteCount = paletteSpan.Length - 1; + for (int y = 0; y < accessor.Height; y++) { - ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = paletteSpan.Length - 1; - for (int y = 0; y < accessor.Height; y++) - { - Span row = accessor.GetRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.DangerousGetRowSpan(y); + Span row = accessor.GetRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.DangerousGetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) - { - row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; - } + for (int x = 0; x < accessor.Width; x++) + { + row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; } - }); - } - - expectedImage.ProcessPixelRows(actualImage, static (expectedAccessor, actualAccessor) => - { - for (int y = 0; y < expectedAccessor.Height; y++) - { - Assert.True(expectedAccessor.GetRowSpan(y).SequenceEqual(actualAccessor.GetRowSpan(y))); } }); } + + expectedImage.ProcessPixelRows(actualImage, static (expectedAccessor, actualAccessor) => + { + for (int y = 0; y < expectedAccessor.Height; y++) + { + Assert.True(expectedAccessor.GetRowSpan(y).SequenceEqual(actualAccessor.GetRowSpan(y))); + } + }); } } From 502a35426225e22ebff965ffaa2be31ecc286880 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 27 Nov 2024 22:21:39 +1000 Subject: [PATCH 2/2] Add tests and fix issues. --- .../Formats/Cur/CurFrameMetadata.cs | 47 +++--- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 42 +++-- .../Formats/Ico/IcoFrameMetadata.cs | 47 +++--- .../Formats/Icon/IconEncoderCore.cs | 4 +- src/ImageSharp/Formats/Pbm/BinaryEncoder.cs | 75 ++++++--- src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs | 22 ++- src/ImageSharp/Formats/Pbm/PlainEncoder.cs | 68 ++++++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 1 + .../Formats/Tiff/TiffEncoderCore.cs | 6 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 8 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 8 + .../Formats/Webp/WebpEncoderCore.cs | 4 + .../Formats/Bmp/BmpEncoderTests.cs | 60 +++++++ .../Formats/Gif/GifEncoderTests.cs | 58 +++++++ .../Formats/Icon/Cur/CurDecoderTests.cs | 8 +- .../Formats/Icon/Cur/CurEncoderTests.cs | 67 ++++++++ .../Formats/Icon/Ico/IcoDecoderTests.cs | 28 ++-- .../Formats/Icon/Ico/IcoEncoderTests.cs | 59 +++++++ .../Formats/Png/PngEncoderTests.cs | 4 +- .../Formats/Qoi/QoiEncoderTests.cs | 59 +++++++ .../Formats/Tga/TgaEncoderTests.cs | 152 +++++++++++------- .../Formats/Tiff/TiffEncoderTests.cs | 5 +- .../Image/ImageTests.Decode_Cancellation.cs | 12 +- .../Image/ImageTests.EncodeCancellation.cs | 131 +++++++++++++++ .../TestUtilities/PausedMemoryStream.cs | 13 +- .../TestUtilities/PausedStream.cs | 4 +- 26 files changed, 784 insertions(+), 208 deletions(-) create mode 100644 tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 4e9a432b16..01b7fbce08 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -48,13 +48,13 @@ private CurFrameMetadata(CurFrameMetadata other) /// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingWidth { get; set; } + public byte? EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingHeight { get; set; } + public byte? EncodingHeight { get; set; } /// /// Gets or sets the number of bits per pixel.
@@ -80,20 +80,6 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; } - byte encodingWidth = metadata.EncodingWidth switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingWidth, - _ => 0 - }; - - byte encodingHeight = metadata.EncodingHeight switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingHeight, - _ => 0 - }; - int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; BmpBitsPerPixel bbpp = bpp switch { @@ -116,8 +102,8 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin { BmpBitsPerPixel = bbpp, Compression = compression, - EncodingWidth = encodingWidth, - EncodingHeight = encodingHeight, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null }; } @@ -138,8 +124,8 @@ public void AfterFrameApply(ImageFrame source, ImageFrame @@ -156,7 +142,7 @@ internal void FromIconDirEntry(IconDirEntry entry) this.HotspotY = entry.BitCount; } - internal IconDirEntry ToIconDirEntry() + internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 @@ -164,8 +150,8 @@ internal IconDirEntry ToIconDirEntry() return new() { - Width = this.EncodingWidth, - Height = this.EncodingHeight, + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Planes = this.HotspotX, BitCount = this.HotspotY, ColorCount = colorCount @@ -233,13 +219,22 @@ private PixelTypeInfo GetPixelTypeInfo() }; } - private static byte Scale(byte? value, int destination, float ratio) + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) { - return (byte)Math.Clamp(destination, 0, 255); + return ClampEncodingDimension(destination); } - return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 3c6e269e43..797e825dc4 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -207,22 +207,29 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile); } - this.EncodeFirstFrame(stream, frameMetadata, quantized); - - // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. - TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); - - this.EncodeAdditionalFrames( - stream, - image, - globalPalette, - derivedTransparencyIndex, - frameMetadata.DisposalMode, - cancellationToken); - - stream.WriteByte(GifConstants.EndIntroducer); + // If the token is cancelled during encoding of frames we must ensure the + // quantized frame is disposed. + try + { + this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken); + + // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. + TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); + + this.EncodeAdditionalFrames( + stream, + image, + globalPalette, + derivedTransparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); + } + finally + { + stream.WriteByte(GifConstants.EndIntroducer); - quantized?.Dispose(); + quantized?.Dispose(); + } } private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex) @@ -310,9 +317,12 @@ private void EncodeAdditionalFrames( private void EncodeFirstFrame( Stream stream, GifFrameMetadata metadata, - IndexedImageFrame quantized) + IndexedImageFrame quantized, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + cancellationToken.ThrowIfCancellationRequested(); + this.WriteGraphicalControlExtension(metadata, stream); Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index a2d1c01391..62aa705cbe 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -41,13 +41,13 @@ private IcoFrameMetadata(IcoFrameMetadata other) /// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingWidth { get; set; } + public byte? EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingHeight { get; set; } + public byte? EncodingHeight { get; set; } /// /// Gets or sets the number of bits per pixel.
@@ -73,20 +73,6 @@ public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; } - byte encodingWidth = metadata.EncodingWidth switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingWidth, - _ => 0 - }; - - byte encodingHeight = metadata.EncodingHeight switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingHeight, - _ => 0 - }; - int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; BmpBitsPerPixel bbpp = bpp switch { @@ -109,8 +95,8 @@ public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin { BmpBitsPerPixel = bbpp, Compression = compression, - EncodingWidth = encodingWidth, - EncodingHeight = encodingHeight, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null }; } @@ -131,8 +117,8 @@ public void AfterFrameApply(ImageFrame source, ImageFrame @@ -147,7 +133,7 @@ internal void FromIconDirEntry(IconDirEntry entry) this.EncodingHeight = entry.Height; } - internal IconDirEntry ToIconDirEntry() + internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 @@ -155,8 +141,8 @@ internal IconDirEntry ToIconDirEntry() return new() { - Width = this.EncodingWidth, - Height = this.EncodingHeight, + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Planes = 1, ColorCount = colorCount, BitCount = this.Compression switch @@ -228,13 +214,22 @@ private PixelTypeInfo GetPixelTypeInfo() }; } - private static byte Scale(byte? value, int destination, float ratio) + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) { - return (byte)Math.Clamp(destination, 0, 255); + return ClampEncodingDimension(destination); } - return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 80c3ec4c31..03e01f912f 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -123,13 +123,13 @@ private void InitHeader(Image image) image.Frames.Select(i => { IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); }).ToArray(), IconFileType.CUR => image.Frames.Select(i => { CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); }).ToArray(), _ => throw new NotSupportedException(), }; diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index dddc629b3e..8b379e4d76 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -17,25 +17,32 @@ internal class BinaryEncoder ///
/// The type of input pixel. /// The configuration. - /// The bytestream to write to. + /// The byte stream to write to. /// The input image. /// The ColorType to use. - /// Data type of the pixles components. - /// + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + /// /// Thrown if an invalid combination of setting is requested. /// - public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType) + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - WriteGrayscale(configuration, stream, image); + WriteGrayscale(configuration, stream, image, cancellationToken); } else if (componentType == PbmComponentType.Short) { - WriteWideGrayscale(configuration, stream, image); + WriteWideGrayscale(configuration, stream, image, cancellationToken); } else { @@ -46,31 +53,28 @@ public static void WritePixels(Configuration configuration, Stream strea { if (componentType == PbmComponentType.Byte) { - WriteRgb(configuration, stream, image); + WriteRgb(configuration, stream, image, cancellationToken); } else if (componentType == PbmComponentType.Short) { - WriteWideRgb(configuration, stream, image); + WriteWideRgb(configuration, stream, image, cancellationToken); } else { throw new ImageFormatException("Component type not supported for Color PBM."); } } - else + else if (componentType == PbmComponentType.Bit) { - if (componentType == PbmComponentType.Bit) - { - WriteBlackAndWhite(configuration, stream, image); - } - else - { - throw new ImageFormatException("Component type not supported for Black & White PBM."); - } + WriteBlackAndWhite(configuration, stream, image, cancellationToken); } } - private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -82,6 +86,8 @@ private static void WriteGrayscale(Configuration configuration, Stream s for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8Bytes( @@ -94,7 +100,11 @@ private static void WriteGrayscale(Configuration configuration, Stream s } } - private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 2; @@ -107,6 +117,8 @@ private static void WriteWideGrayscale(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL16Bytes( @@ -119,7 +131,11 @@ private static void WriteWideGrayscale(Configuration configuration, Stre } } - private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 3; @@ -132,6 +148,8 @@ private static void WriteRgb(Configuration configuration, Stream stream, for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb24Bytes( @@ -144,7 +162,11 @@ private static void WriteRgb(Configuration configuration, Stream stream, } } - private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 6; @@ -157,6 +179,8 @@ private static void WriteWideRgb(Configuration configuration, Stream str for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb48Bytes( @@ -169,7 +193,12 @@ private static void WriteWideRgb(Configuration configuration, Stream str } } - private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteBlackAndWhite( + Configuration + configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -181,6 +210,8 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index 843f1880e6..e0330ca6b4 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -68,8 +68,7 @@ public void Encode(Image image, Stream stream, CancellationToken byte signature = this.DeduceSignature(); this.WriteHeader(stream, signature, image.Size); - - this.WritePixels(stream, image.Frames.RootFrame); + this.WritePixels(stream, image.Frames.RootFrame, cancellationToken); stream.Flush(); } @@ -167,16 +166,29 @@ private void WriteHeader(Stream stream, byte signature, Size pixelSize) /// /// The containing pixel data. /// - private void WritePixels(Stream stream, ImageFrame image) + /// The token to monitor for cancellation requests. + private void WritePixels(Stream stream, ImageFrame image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (this.encoding == PbmEncoding.Plain) { - PlainEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType); + PlainEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); } else { - BinaryEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType); + BinaryEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs index 29260f54aa..bab508720d 100644 --- a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Pixel encoding methods for the PBM plain encoding. /// -internal class PlainEncoder +internal static class PlainEncoder { private const byte NewLine = 0x0a; private const byte Space = 0x20; @@ -31,45 +31,56 @@ internal class PlainEncoder ///
/// The type of input pixel. /// The configuration. - /// The bytestream to write to. + /// The byte stream to write to. /// The input image. /// The ColorType to use. - /// Data type of the pixles components. - public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType) + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - WriteGrayscale(configuration, stream, image); + WriteGrayscale(configuration, stream, image, cancellationToken); } else { - WriteWideGrayscale(configuration, stream, image); + WriteWideGrayscale(configuration, stream, image, cancellationToken); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - WriteRgb(configuration, stream, image); + WriteRgb(configuration, stream, image, cancellationToken); } else { - WriteWideRgb(configuration, stream, image); + WriteWideRgb(configuration, stream, image, cancellationToken); } } else { - WriteBlackAndWhite(configuration, stream, image); + WriteBlackAndWhite(configuration, stream, image, cancellationToken); } // Write EOF indicator, as some encoders expect it. stream.WriteByte(Space); } - private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -83,6 +94,8 @@ private static void WriteGrayscale(Configuration configuration, Stream s for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( configuration, @@ -102,7 +115,11 @@ private static void WriteGrayscale(Configuration configuration, Stream s } } - private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -116,6 +133,8 @@ private static void WriteWideGrayscale(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL16( configuration, @@ -135,7 +154,11 @@ private static void WriteWideGrayscale(Configuration configuration, Stre } } - private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -149,6 +172,8 @@ private static void WriteRgb(Configuration configuration, Stream stream, for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb24( configuration, @@ -174,7 +199,11 @@ private static void WriteRgb(Configuration configuration, Stream stream, } } - private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -188,6 +217,8 @@ private static void WriteWideRgb(Configuration configuration, Stream str for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb48( configuration, @@ -213,7 +244,11 @@ private static void WriteWideRgb(Configuration configuration, Stream str } } - private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteBlackAndWhite( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -227,6 +262,8 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( configuration, @@ -236,8 +273,7 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre int written = 0; for (int x = 0; x < width; x++) { - byte value = (rowSpan[x].PackedValue < 128) ? One : Zero; - plainSpan[written++] = value; + plainSpan[written++] = (rowSpan[x].PackedValue < 128) ? One : Zero; plainSpan[written++] = Space; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 05220e8019..ea36d9fe1e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -226,6 +226,7 @@ public void Encode(Image image, Stream stream, CancellationToken bool userAnimateRootFrame = this.animateRootFrame == true; if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1) { + cancellationToken.ThrowIfCancellationRequested(); FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); currentFrameIndex++; diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 4f6985f9db..da55ef9f9b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -137,7 +137,7 @@ public void Encode(Image image, Stream stream, CancellationToken long ifdMarker = WriteHeader(writer, buffer); - Image? metadataImage = image; + Image? imageMetadata = image; foreach (ImageFrame frame in image.Frames) { @@ -154,8 +154,8 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame encodingFrame = clonedFrame ?? frame; - ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); - metadataImage = null; + ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); + imageMetadata = null; } finally { diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index d22d357fe3..e4ebe14731 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -388,7 +388,13 @@ public void EncodeStatic(Stream stream, Image image) /// Flag indicating, if an animation parameter is present. /// The image to encode from. /// A indicating whether the frame contains an alpha channel. - private bool Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image) + private bool Encode( + Stream stream, + ImageFrame frame, + Rectangle bounds, + WebpFrameMetadata frameMetadata, + bool hasAnimation, + Image image) where TPixel : unmanaged, IPixel { int width = bounds.Width; diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 622fe0181e..2e459ff58e 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Webp; ///
public sealed class WebpEncoder : AnimatedImageEncoder { + /// + /// Initializes a new instance of the class. + /// + public WebpEncoder() + + // Match the default behavior of the native reference encoder. + => this.TransparentColorMode = TransparentColorMode.Clear; + /// /// Gets the webp file format used. Either lossless or lossy. /// Defaults to lossy. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index b8f27a8326..b3270786d7 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -166,6 +166,9 @@ public void Encode(Image image, Stream stream, CancellationToken // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); + + cancellationToken.ThrowIfCancellationRequested(); + hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation); if (hasAnimation) @@ -304,6 +307,7 @@ public void Encode(Image image, Stream stream, CancellationToken } else { + cancellationToken.ThrowIfCancellationRequested(); encoder.EncodeStatic(stream, image); encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index d68ec47557..09ef49a61e 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -397,6 +397,66 @@ public void Encode_Issue2467(TestImageProvider provider, BmpBits reencodedImage.CompareToOriginal(provider); } + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + BmpEncoder encoder = new() + { + BitsPerPixel = BmpBitsPerPixel.Bit32, + SupportTransparency = true, + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } + private static void TestBmpEncoderCore( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index c08db84eb6..f12f66186e 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -361,4 +361,62 @@ public void Encode_Animated_VisualTest(TestImageProvider provide provider.Utility.SaveTestOutputFile(image, "png", new PngEncoder(), "animated"); provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + GifEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index f7ee7614af..bac52fc728 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -20,8 +20,8 @@ public void CurDecoder_Decode(TestImageProvider provider) using Image image = provider.GetImage(CurDecoder.Instance); CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); - Assert.Equal(image.Width, meta.EncodingWidth); - Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(image.Width, meta.EncodingWidth.Value); + Assert.Equal(image.Height, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -33,8 +33,8 @@ public void CurDecoder_Decode2(TestImageProvider provider) { using Image image = provider.GetImage(CurDecoder.Instance); CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); - Assert.Equal(image.Width, meta.EncodingWidth); - Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(image.Width, meta.EncodingWidth.Value); + Assert.Equal(image.Height, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index 59c40c9245..bf94e1d489 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; @@ -63,4 +64,70 @@ public void CanConvertFromIco(TestImageProvider provider) Assert.Equal(icoFrame.EncodingHeight, curFrame.EncodingHeight); } } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + CurEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + Span rowSpanOpp = accessor.GetRowSpan(accessor.Height - y - 1); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + if (expectedColor != rowSpan[x]) + { + var xx = 0; + } + + + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index bc46df0955..e076ccab60 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -53,8 +53,8 @@ public void Bpp1Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit1, meta.BmpBitsPerPixel); } @@ -89,8 +89,8 @@ public void Bpp24Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit24, meta.BmpBitsPerPixel); } @@ -125,8 +125,8 @@ public void Bpp32Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -160,8 +160,8 @@ public void Bpp4Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit4, meta.BmpBitsPerPixel); } @@ -196,8 +196,8 @@ public void Bpp8Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit8, meta.BmpBitsPerPixel); } @@ -226,8 +226,8 @@ public void InvalidPngTest(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Png, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -324,8 +324,8 @@ public void IcoFakeTest(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index 751db384d7..4c7438d568 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; @@ -62,4 +63,62 @@ public void CanConvertFromCur(TestImageProvider provider) Assert.Equal(curFrame.ColorTable, icoFrame.ColorTable); } } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + IcoEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b6bb243afa..b4995d77b6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -340,10 +340,10 @@ public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth) [InlineData(PngColorType.Palette)] [InlineData(PngColorType.RgbWithAlpha)] [InlineData(PngColorType.GrayscaleWithAlpha)] - public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType) + public void Encode_WithTransparentColorBehaviorClear_Works(PngColorType colorType) { // arrange - Image image = new(50, 50); + using Image image = new(50, 50); PngEncoder encoder = new() { TransparentColorMode = TransparentColorMode.Clear, diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs index 32ade4a1e9..9da9ad3275 100644 --- a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -41,4 +42,62 @@ public static void Encode(TestImageProvider provider, QoiChannel Assert.Equal(qoiMetadata.Channels, channels); Assert.Equal(qoiMetadata.ColorSpace, colorSpace); } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + QoiEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 615e0fc921..adf0b4353d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -10,6 +11,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga; [Trait("Format", "Tga")] +[ValidateDisposedMemoryAllocations] public class TgaEncoderTests { public static readonly TheoryData BitsPerPixel = @@ -32,43 +34,35 @@ public class TgaEncoderTests [MemberData(nameof(TgaBitsPerPixelFiles))] public void TgaEncoder_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) { - var options = new TgaEncoder(); + TgaEncoder options = new(); - 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)) - { - TgaMetadata meta = output.Metadata.GetTgaMetadata(); - Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); - } - } - } + 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); + TgaMetadata meta = output.Metadata.GetTgaMetadata(); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } [Theory] [MemberData(nameof(TgaBitsPerPixelFiles))] public void TgaEncoder_WithCompression_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) { - var options = new TgaEncoder() { Compression = TgaCompression.RunLength }; - 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)) - { - TgaMetadata meta = output.Metadata.GetTgaMetadata(); - Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); - } - } - } + TgaEncoder options = new() { Compression = TgaCompression.RunLength }; + 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); + TgaMetadata meta = output.Metadata.GetTgaMetadata(); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } [Theory] @@ -136,17 +130,13 @@ public void TgaEncoder_DoesNotAlwaysUseRunLengthPackets(TestImageProvide [Fact] public void TgaEncoder_RunLengthDoesNotCrossRowBoundaries() { - var options = new TgaEncoder() { Compression = TgaCompression.RunLength }; + TgaEncoder options = new() { Compression = TgaCompression.RunLength }; - using (var input = new Image(30, 30)) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - byte[] imageBytes = memStream.ToArray(); - Assert.Equal(138, imageBytes.Length); - } - } + using Image input = new(30, 30); + using MemoryStream memStream = new(); + input.Save(memStream, options); + byte[] imageBytes = memStream.ToArray(); + Assert.Equal(138, imageBytes.Length); } [Theory] @@ -159,6 +149,65 @@ public void TgaEncoder_WorksWithDiscontiguousBuffers(TestImageProvider image = new(50, 50); + TgaEncoder encoder = new() + { + BitsPerPixel = TgaBitsPerPixel.Bit32, + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } + private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, @@ -167,20 +216,15 @@ private static void TestTgaEncoderCore( float compareTolerance = 0.01f) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression }; + using Image image = provider.GetImage(); + TgaEncoder encoder = new() { BitsPerPixel = bitsPerPixel, Compression = compression }; - using (var memStream = new MemoryStream()) - { - image.DebugSave(provider, encoder); - image.Save(memStream, encoder); - memStream.Position = 0; - using (var encodedImage = (Image)Image.Load(memStream)) - { - ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); - } - } - } + using MemoryStream memStream = new(); + image.DebugSave(provider, encoder); + image.Save(memStream, encoder); + memStream.Position = 0; + + using Image encodedImage = (Image)Image.Load(memStream); + ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 0d59625ca7..674ca5c5bb 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -256,7 +256,7 @@ public void TiffEncoder_WritesIfdOffsetAtWordBoundary() TiffEncoder tiffEncoder = new(); using MemoryStream memStream = new(); using Image image = new(1, 1); - byte[] expectedIfdOffsetBytes = { 12, 0 }; + byte[] expectedIfdOffsetBytes = [12, 0]; // act image.Save(memStream, tiffEncoder); @@ -613,8 +613,7 @@ public void TiffEncode_WorksWithDiscontiguousBuffers(TestImageProvider image = provider.GetImage(); - TiffEncoder encoder = new() - { PhotometricInterpretation = photometricInterpretation }; + TiffEncoder encoder = new() { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs index d8d9f4fe2a..0656f9e0bc 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -12,8 +12,8 @@ public class Decode_Cancellation : ImageLoadTestBase { public Decode_Cancellation() => this.TopLevelConfiguration.StreamProcessingBufferSize = 128; - public static readonly string[] TestFileForEachCodec = new[] - { + public static readonly string[] TestFileForEachCodec = + [ TestImages.Jpeg.Baseline.Snake, // TODO: Figure out Unix cancellation failures, and validate cancellation for each decoder. @@ -24,7 +24,7 @@ public class Decode_Cancellation : ImageLoadTestBase //TestImages.Tga.Bit32BottomRight, //TestImages.Webp.Lossless.WithExif, //TestImages.Pbm.GrayscaleBinaryWide - }; + ]; public static object[][] IdentifyData { get; } = TestFileForEachCodec.Select(f => new object[] { f }).ToArray(); @@ -32,16 +32,16 @@ public class Decode_Cancellation : ImageLoadTestBase [MemberData(nameof(IdentifyData))] public async Task IdentifyAsync_PreCancelled(string file) { - using FileStream fs = File.OpenRead(TestFile.GetInputFileFullPath(file)); + await using FileStream fs = File.OpenRead(TestFile.GetInputFileFullPath(file)); CancellationToken preCancelled = new(canceled: true); await Assert.ThrowsAnyAsync(async () => await Image.IdentifyAsync(fs, preCancelled)); } private static TheoryData CreateLoadData() { - double[] percentages = new[] { 0, 0.3, 0.7 }; + double[] percentages = [0, 0.3, 0.7]; - TheoryData data = new(); + TheoryData data = []; foreach (string file in TestFileForEachCodec) { diff --git a/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs new file mode 100644 index 0000000000..f3b1a01c94 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests; + +public partial class ImageTests +{ + [ValidateDisposedMemoryAllocations] + public class Encode_Cancellation + { + [Fact] + public async Task Encode_PreCancellation_Bmp() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsBmpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Cur() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsCurAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Gif() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Gif() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Ico() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsIcoAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Jpeg() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsJpegAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Pbm() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsPbmAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Png() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Png() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Qoi() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsQoiAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Tga() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsTgaAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Tiff() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsTiffAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Webp() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Webp() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs index ae4af24f14..d1149dd004 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs @@ -6,9 +6,10 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities; /// -/// is a variant of that derives from instead of encapsulating it. -/// It is used to test decoder REacellation without relying on of our standard prefetching of arbitrary streams to -/// on asynchronous path. +/// is a variant of that derives from +/// instead of encapsulating it. +/// It is used to test decoder cancellation without relying on of our standard prefetching of arbitrary streams +/// to on asynchronous path. /// public class PausedMemoryStream : MemoryStream, IPausedStream { @@ -108,11 +109,11 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override bool CanWrite => base.CanWrite; - public override void Flush() => this.Await(() => base.Flush()); + public override void Flush() => this.Await(base.Flush); public override int Read(byte[] buffer, int offset, int count) => this.Await(() => base.Read(buffer, offset, count)); - public override long Seek(long offset, SeekOrigin origin) => this.Await(() => base.Seek(offset, origin)); + public override long Seek(long offset, SeekOrigin loc) => this.Await(() => base.Seek(offset, loc)); public override void SetLength(long value) => this.Await(() => base.SetLength(value)); @@ -124,7 +125,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override void WriteByte(byte value) => this.Await(() => base.WriteByte(value)); - public override int ReadByte() => this.Await(() => base.ReadByte()); + public override int ReadByte() => this.Await(base.ReadByte); public override void CopyTo(Stream destination, int bufferSize) { diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index 3c780f3474..42ed6b0d5e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -115,7 +115,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override long Position { get => this.innerStream.Position; set => this.innerStream.Position = value; } - public override void Flush() => this.Await(() => this.innerStream.Flush()); + public override void Flush() => this.Await(this.innerStream.Flush); public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count)); @@ -131,7 +131,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value)); - public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); + public override int ReadByte() => this.Await(this.innerStream.ReadByte); protected override void Dispose(bool disposing) {