From 8dd4f35d5e126e0f429e27a9ba8f44b1445cebc0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 25 Nov 2024 21:56:54 +1000 Subject: [PATCH] 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 000000000..f753e7282 --- /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 7c92d3e46..321a559b1 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 @@ internal sealed class BmpEncoderCore // 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore /// /// 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 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra32Bytes( configuration, @@ -421,7 +455,12 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgr24Bytes( configuration, @@ -448,7 +489,12 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore for (int y = pixels.Height - 1; y >= 0; y--) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra5551Bytes( @@ -476,21 +524,32 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore /// /// 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 @@ internal sealed class BmpEncoderCore } 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 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore /// 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore 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 @@ internal sealed class BmpEncoderCore } 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 000000000..a979fdf6f --- /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 a99b5862d..3d6990478 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -665,7 +665,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore 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 0ed7e8c98..3c6e269e4 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 @@ internal sealed class GifEncoderCore this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.backgroundColor = encoder.BackgroundColor; this.repeatCount = encoder.RepeatCount; + this.transparentColorMode = encoder.TransparentColorMode; } /// @@ -131,18 +134,40 @@ internal sealed class GifEncoderCore } } + // 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 @@ internal sealed class GifEncoderCore // 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 @@ internal sealed class GifEncoderCore // 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 @@ internal sealed class GifEncoderCore 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 44431aa9a..d2c3ad690 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 e88b3ecf0..5edf6e40e 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 4b973d511..80c3ec4c3 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -63,7 +63,6 @@ internal abstract class IconEncoderCore 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 @@ internal abstract class IconEncoderCore 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 @@ internal abstract class IconEncoderCore // 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 d9f71e1b5..63e675b50 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -68,12 +68,6 @@ public class PngEncoder : QuantizingAnimatedImageEncoder /// 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 398c80634..05220e801 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,6 +7,7 @@ using System.IO.Hashing; 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 @@ internal sealed class PngEncoderCore : IDisposable 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 @@ internal sealed class PngEncoderCore : IDisposable 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 @@ internal sealed class PngEncoderCore : IDisposable 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 76a89608b..000000000 --- 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 b9c2078b3..1da9caffb 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 88d87a382..872cec3fd 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -55,7 +55,7 @@ internal class QoiEncoderCore 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 @@ internal class QoiEncoderCore 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 @@ internal class QoiEncoderCore 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 09b12e608..a4630a464 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 1e05a9f71..e2ea9c4fe 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 @@ internal sealed class TgaEncoderCore this.memoryAllocator = memoryAllocator; this.bitsPerPixel = encoder.BitsPerPixel; this.compression = encoder.Compression; + this.transparentColorMode = encoder.TransparentColorMode; } /// @@ -103,16 +106,33 @@ internal sealed class TgaEncoderCore 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 b560067f3..4f6985f9d 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 @@ internal sealed class TiffEncoderCore 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 000000000..39986b502 --- /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 46030dde3..fd6f508e4 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -49,7 +49,7 @@ internal static class AlphaEncoder 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 2170eb198..736070a1c 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs @@ -47,7 +47,7 @@ internal static unsafe class PredictorEncoder int[][] bestHisto, bool nearLossless, int nearLosslessQuality, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool lowEffort) { @@ -202,7 +202,7 @@ internal static unsafe class PredictorEncoder int[][] histoArgb, int[][] bestHisto, int maxQuantization, - WebpTransparentColorMode transparentColorMode, + TransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, Span modes, @@ -340,19 +340,20 @@ internal static unsafe class PredictorEncoder 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 @@ internal static unsafe class PredictorEncoder } } } +#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 @@ internal static unsafe class PredictorEncoder 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 e07724969..f08844839 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 @@ internal class Vp8LEncoder : IDisposable 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 bfaaa831e..b74337ef3 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -343,7 +343,7 @@ internal class WebpAnimationDecoder : IDisposable 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 226719c79..622fe0181 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 37d2ae0a1..b8f27a832 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 @@ internal sealed class WebpEncoderCore // 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 @@ internal sealed class WebpEncoderCore 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 @@ internal sealed class WebpEncoderCore 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 @@ internal sealed class WebpEncoderCore 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 c12b8ed97..000000000 --- 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 fdde5019e..686292bc7 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -56,7 +56,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// 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 2287f65cd..de71e77ca 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -429,7 +429,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource 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 2eb05ea93..f0fa1438d 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -25,27 +25,65 @@ public static class Buffer2DExtensions 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 @@ public static class Buffer2DExtensions /// 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 @@ public static class Buffer2DExtensions /// 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 @@ public static class Buffer2DExtensions /// 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 @@ public static class Buffer2DExtensions /// /// 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 033b0a25a..f4b257b58 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -107,7 +107,7 @@ public readonly struct Buffer2DRegion [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 b4b1ffc6f..e2e933f3c 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -36,8 +36,13 @@ internal static class MemoryGroupExtensions /// /// 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 4d95e060d..63c489508 100644 --- a/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs +++ b/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs @@ -61,9 +61,7 @@ internal class DefaultImageProcessorContext : IInternalImageProcessingCo /// 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 @@ internal class DefaultImageProcessorContext : IInternalImageProcessingCo // 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 713d4d5b7..676acee0f 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 partial class ProcessingExtensions /// 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 partial class ProcessingExtensions { Configuration configuration = source.Configuration; - var interest = Rectangle.Intersect(bounds, source.Bounds()); + Rectangle interest = Rectangle.Intersect(bounds, source.Bounds); int startY = interest.Y; int startX = interest.X; int endY = interest.Height; diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 73c7c3302..e17de49d7 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization; /// /// Performs Bradley Adaptive Threshold filter against an image. /// +/// The pixel format. internal class AdaptiveThresholdProcessor : ImageProcessor where TPixel : unmanaged, IPixel { @@ -30,7 +31,7 @@ internal class AdaptiveThresholdProcessor : ImageProcessor /// 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 @@ internal class AdaptiveThresholdProcessor : ImageProcessor 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 1c76ea6a4..ad87f36c1 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -38,8 +38,8 @@ internal class BinaryThresholdProcessor : ImageProcessor 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 @@ internal class BinaryThresholdProcessor : ImageProcessor { 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 5931b7c40..a96fa1993 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -75,12 +75,12 @@ internal class BokehBlurProcessor : ImageProcessor /// 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 @@ internal class BokehBlurProcessor : ImageProcessor } 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 @@ internal class BokehBlurProcessor : ImageProcessor // 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 @@ internal class BokehBlurProcessor : ImageProcessor } 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 @@ internal class BokehBlurProcessor : ImageProcessor // 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 @@ internal class BokehBlurProcessor : ImageProcessor 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 @@ internal class BokehBlurProcessor : ImageProcessor 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 @@ internal class BokehBlurProcessor : ImageProcessor /// [MethodImpl(InliningOptions.ShortMethod)] public int GetRequiredBufferLength(Rectangle bounds) - { - return bounds.Width; - } + => bounds.Width; /// [MethodImpl(InliningOptions.ShortMethod)] @@ -391,7 +389,7 @@ internal class BokehBlurProcessor : ImageProcessor 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 8f5ddd169..02e06db49 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -62,7 +62,7 @@ internal class Convolution2DProcessor : ImageProcessor 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 cdd8ff8ae..1bbbdb350 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -98,7 +98,7 @@ internal class Convolution2PassProcessor : ImageProcessor { 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 9b4659929..feaaf30ce 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -89,7 +89,7 @@ internal class ConvolutionProcessor : ImageProcessor 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 a1fa4db97..eae748166 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -58,7 +58,7 @@ internal class EdgeDetectorCompassProcessor : ImageProcessor /// 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 fe3a29d43..3e75d8b84 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -29,7 +29,7 @@ internal sealed class MedianBlurProcessor : ImageProcessor 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 982cc7d46..7e672393c 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -46,7 +46,7 @@ internal sealed class PaletteDitherProcessor : ImageProcessor /// 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 847a211a5..06cfa49b3 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs @@ -36,14 +36,12 @@ internal sealed class PixelRowDelegateProcessor : IImageProcessor /// 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 @@ internal sealed class PixelRowDelegateProcessor : IImageProcessor [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 36bb327cf..d38ffc801 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 @@ internal sealed class PixelRowDelegateProcessor : ImageProces /// 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 68000ba3e..c828b95b6 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs @@ -32,7 +32,7 @@ internal class PixelateProcessor : ImageProcessor /// 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 510913964..37286086c 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 93e600106..41560d120 100644 --- a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs @@ -23,9 +23,9 @@ internal sealed class OpaqueProcessor : ImageProcessor 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 6f4493f95..606789af9 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor( this.Configuration, interest, @@ -91,7 +89,7 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor( this.Configuration, interest, @@ -136,10 +134,10 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); @@ -197,10 +194,10 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor : HistogramEqualizat { 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 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat 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 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat [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 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat 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 1e4345825..9ed314ee3 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs @@ -35,7 +35,7 @@ internal class BackgroundColorProcessor : ImageProcessor 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 @@ internal class BackgroundColorProcessor : ImageProcessor 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 19ce6c417..d73e7bea1 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs @@ -40,7 +40,7 @@ internal class GlowProcessor : ImageProcessor 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 @@ internal class GlowProcessor : ImageProcessor 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 a327deec1..b08cd898e 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs @@ -40,7 +40,7 @@ internal class VignetteProcessor : ImageProcessor 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 @@ internal class VignetteProcessor : ImageProcessor 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 da2580fed..16bb412c7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -32,7 +32,7 @@ internal class QuantizeProcessor : ImageProcessor /// 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 fc1dda6be..6d2200b8a 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 class QuantizerUtilities 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 class QuantizerUtilities 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 class QuantizerUtilities 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 class QuantizerUtilities 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 888d51320..b3919e584 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 @@ internal class AffineTransformProcessor : TransformProcessor, IR 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 @@ internal class AffineTransformProcessor : TransformProcessor, IR { 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 @@ internal class AffineTransformProcessor : TransformProcessor, IR 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 14da3ac89..1adda1d04 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 @@ internal class FlipProcessor : ImageProcessor /// 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 068f69ceb..16b0070e9 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 @@ internal class ProjectiveTransformProcessor : TransformProcessor 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 @@ internal class ProjectiveTransformProcessor : TransformProcessor { 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 @@ internal class ProjectiveTransformProcessor : TransformProcessor 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 3ffb1ab51..8d3ded09f 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 @@ internal class RotateProcessor : AffineTransformProcessor /// 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 d78640549..31b6cbdde 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 class EncodeWebp 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 53a795c64..b6bb243af 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -346,7 +346,7 @@ public partial class PngEncoderTests 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 0aa56e252..f82fa65df 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -311,7 +311,7 @@ public class WebpEncoderTests { 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 403865e66..5e5887c92 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 59a0ad427..b59542482 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 class QuantizedImageTests 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 class QuantizedImageTests 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 class QuantizedImageTests public void Issue1505(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 ccb79debd..74f2fc3b4 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 class WuQuantizerTests 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 class WuQuantizerTests [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 class WuQuantizerTests } 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 class WuQuantizerTests 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))); + } + }); } }