From b4567a4ac7f74c804df590e4bcd3d88ba4c84b89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2024 13:01:16 +1000 Subject: [PATCH 1/3] Normalize encoders and update refs --- .../Formats/FormatConnectingMetadata.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoder.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 94 ++++++++++++++++--- .../Formats/IAnimatedImageEncoder.cs | 43 +++++++++ .../Formats/IQuantizingImageEncoder.cs | 50 ++++++++++ src/ImageSharp/Formats/Png/PngEncoder.cs | 3 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 48 ++++++++-- .../Formats/QuantizingImageEncoder.cs | 22 ----- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 4 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 50 ++++++++-- .../Formats/Webp/WebpFrameMetadata.cs | 22 +++-- tests/Directory.Build.targets | 16 ++-- .../Codecs/Tga/DecodeTga.cs | 10 +- .../Codecs/Webp/DecodeWebp.cs | 25 ++--- .../Codecs/Webp/EncodeWebp.cs | 10 +- .../ImageSharp.Benchmarks.csproj | 4 +- .../LoadResizeSaveStressRunner.cs | 63 +++++++------ .../Formats/Gif/GifEncoderTests.cs | 2 +- .../Formats/Png/PngEncoderTests.cs | 4 +- .../Formats/WebP/WebpEncoderTests.cs | 12 +-- .../Normalization/MagickCompareTests.cs | 42 ++++----- .../ImageComparison/ImageComparingUtils.cs | 39 ++++---- .../ReferenceCodecs/MagickReferenceDecoder.cs | 4 +- 26 files changed, 391 insertions(+), 194 deletions(-) create mode 100644 src/ImageSharp/Formats/IAnimatedImageEncoder.cs create mode 100644 src/ImageSharp/Formats/IQuantizingImageEncoder.cs delete mode 100644 src/ImageSharp/Formats/QuantizingImageEncoder.cs diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index baf0a35457..9cfe40f385 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -45,7 +45,7 @@ public class FormatConnectingMetadata /// Gets the default background color of the canvas when animating. /// This color may be used to fill the unused space on the canvas around the frames, /// as well as the transparent pixels of the first frame. - /// The background color is also used when the disposal mode is . + /// The background color is also used when a frame disposal mode is . /// /// /// Defaults to . diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 6cb8f9d8ce..37b585c618 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Image encoder for writing image data to a stream in gif format. /// -public sealed class GifEncoder : QuantizingImageEncoder +public sealed class GifEncoder : QuantizingAnimatedImageEncoder { /// /// Gets the color table mode: Global or local. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 2e05ef782f..757b8320dc 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -54,6 +54,19 @@ internal sealed class GifEncoderCore /// private readonly IPixelSamplingStrategy pixelSamplingStrategy; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + /// /// Initializes a new instance of the class. /// @@ -68,6 +81,8 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder) this.hasQuantizer = encoder.Quantizer is not null; this.colorTableMode = encoder.ColorTableMode; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; } /// @@ -141,9 +156,17 @@ public void Encode(Image image, Stream stream, CancellationToken frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); } - byte backgroundIndex = derivedTransparencyIndex >= 0 - ? frameMetadata.TransparencyIndex - : gifMetadata.BackgroundColorIndex; + byte backgroundIndex; + if (this.backgroundColor.HasValue) + { + backgroundIndex = GetBackgroundIndex(quantized, this.backgroundColor.Value); + } + else + { + backgroundIndex = derivedTransparencyIndex >= 0 + ? frameMetadata.TransparencyIndex + : gifMetadata.BackgroundColorIndex; + } // Get the number of bits. int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); @@ -161,7 +184,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Write application extensions. XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; - this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); + this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile); } this.EncodeFirstFrame(stream, frameMetadata, quantized); @@ -169,7 +192,13 @@ public void Encode(Image image, Stream stream, CancellationToken // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); - this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode); + this.EncodeAdditionalFrames( + stream, + image, + globalPalette, + derivedTransparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); stream.WriteByte(GifConstants.EndIntroducer); @@ -194,7 +223,8 @@ private void EncodeAdditionalFrames( Image image, ReadOnlyMemory globalPalette, int globalTransparencyIndex, - FrameDisposalMode previousDisposalMode) + FrameDisposalMode previousDisposalMode, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (image.Frames.Count == 1) @@ -213,6 +243,16 @@ private void EncodeAdditionalFrames( for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + if (hasPaletteQuantizer) + { + paletteQuantizer.Dispose(); + } + + return; + } + // Gather the metadata for this frame. ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; @@ -291,6 +331,10 @@ private void EncodeAdditionalFrame( ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + // Deduplicate and quantize the frame capturing only required parts. (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -299,7 +343,7 @@ private void EncodeAdditionalFrame( currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, true); using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( @@ -428,14 +472,12 @@ private IndexedImageFrame QuantizeAdditionalFrameAndUpdateMetadata (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue); /// - /// Returns the index of the most transparent color in the palette. + /// Returns the index of the transparent color in the palette. /// /// The current quantized frame. /// The current gif frame metadata. /// The pixel format. - /// - /// The . - /// + /// The . private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata) where TPixel : unmanaged, IPixel { @@ -463,6 +505,36 @@ private static int GetTransparentIndex(IndexedImageFrame? quanti return index; } + /// + /// Returns the index of the background color in the palette. + /// + /// The current quantized frame. + /// The background color to match. + /// The pixel format. + /// The . + private static byte GetBackgroundIndex(IndexedImageFrame? quantized, Color background) + where TPixel : unmanaged, IPixel + { + int index = -1; + if (quantized != null) + { + TPixel backgroundPixel = background.ToPixel(); + ReadOnlySpan palette = quantized.Palette.Span; + for (int i = 0; i < palette.Length; i++) + { + if (!backgroundPixel.Equals(palette[i])) + { + continue; + } + + index = i; + break; + } + } + + return (byte)Numerics.Clamp(index, 0, 255); + } + /// /// Writes the file header signature and version to the stream. /// diff --git a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs new file mode 100644 index 0000000000..44431aa9a4 --- /dev/null +++ b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for all image encoders that allow encoding animation sequences. +/// +public interface IAnimatedImageEncoder +{ + /// + /// Gets the default background color of the canvas when animating in supported encoders. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + Color? BackgroundColor { get; } + + /// + /// Gets the number of times any animation is repeated in supported encoders. + /// + ushort? RepeatCount { get; } + + /// + /// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders. + /// + bool? AnimateRootFrame { get; } +} + +/// +/// Acts as a base class for all image encoders that allow encoding animation sequences. +/// +public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder +{ + /// + public Color? BackgroundColor { get; init; } + + /// + public ushort? RepeatCount { get; init; } + + /// + public bool? AnimateRootFrame { get; init; } = true; +} diff --git a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs new file mode 100644 index 0000000000..e88b3ecf02 --- /dev/null +++ b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for all image encoders that allow color palette generation via quantization. +/// +public interface IQuantizingImageEncoder +{ + /// + /// Gets the quantizer used to generate the color palette. + /// + IQuantizer? Quantizer { get; } + + /// + /// Gets the used for quantization when building color palettes. + /// + IPixelSamplingStrategy PixelSamplingStrategy { get; } +} + +/// +/// Acts as a base class for all image encoders that allow color palette generation via quantization. +/// +public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder +{ + /// + public IQuantizer? Quantizer { get; init; } + + /// + public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); +} + +/// +/// Acts as a base class for all image encoders that allow color palette generation via quantization when +/// encoding animation sequences. +/// +public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder +{ + /// + public Color? BackgroundColor { get; } + + /// + public ushort? RepeatCount { get; } + + /// + public bool? AnimateRootFrame { get; } +} diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index dcbaf3140d..d9f71e1b56 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -1,6 +1,5 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Image encoder for writing image data to a stream in png format. /// -public class PngEncoder : QuantizingImageEncoder +public class PngEncoder : QuantizingAnimatedImageEncoder { /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 978b9184e9..398c80634c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -123,6 +123,24 @@ internal sealed class PngEncoderCore : IDisposable /// private int derivedTransparencyIndex = -1; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + + /// + /// Whether the root frame is shown as part of the animated sequence. + /// + private readonly bool? animateRootFrame; + /// /// A reusable Crc32 hashing instance. /// @@ -139,6 +157,9 @@ public PngEncoderCore(Configuration configuration, PngEncoder encoder) this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; this.quantizer = encoder.Quantizer; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; + this.animateRootFrame = encoder.AnimateRootFrame; } /// @@ -171,7 +192,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (clearTransparency) { currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame); + ClearTransparentPixels(currentFrame, Color.Transparent); } // Do not move this. We require an accurate bit depth for the header chunk. @@ -194,11 +215,15 @@ public void Encode(Image image, Stream stream, CancellationToken if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount); + this.WriteAnimationControlChunk( + stream, + (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), + this.repeatCount ?? pngMetadata.RepeatCount); } // If the first frame isn't animated, write it as usual and skip it when writing animated frames - if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1) + bool userAnimateRootFrame = this.animateRootFrame == true; + if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1) { FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); @@ -235,12 +260,20 @@ public void Encode(Image image, Stream stream, CancellationToken for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + 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( @@ -249,12 +282,12 @@ public void Encode(Image image, Stream stream, CancellationToken currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend); if (clearTransparency) { - ClearTransparentPixels(encodingFrame); + ClearTransparentPixels(encodingFrame, background); } // Each frame control sequence number must be incremented by the number of frame data chunks that follow. @@ -291,12 +324,13 @@ public void Dispose() /// /// The type of the pixel. /// The cloned image frame where the transparent pixels will be changed. - private static void ClearTransparentPixels(ImageFrame clone) + /// 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.Transparent.ToPixel(); + Rgba32 transparent = color.ToPixel(); for (int y = 0; y < accessor.Height; y++) { Span span = accessor.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/QuantizingImageEncoder.cs b/src/ImageSharp/Formats/QuantizingImageEncoder.cs deleted file mode 100644 index 330d8988c7..0000000000 --- a/src/ImageSharp/Formats/QuantizingImageEncoder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors.Quantization; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Acts as a base class for all image encoders that allow color palette generation via quantization. -/// -public abstract class QuantizingImageEncoder : ImageEncoder -{ - /// - /// Gets the quantizer used to generate the color palette. - /// - public IQuantizer? Quantizer { get; init; } - - /// - /// Gets the used for quantization when building color palettes. - /// - public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); -} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 244691e77e..e077249696 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -236,7 +236,7 @@ public Vp8LEncoder( /// public Vp8LHashChain HashChain { get; } - public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation) + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation, ushort? repeatCount) where TPixel : unmanaged, IPixel { // Write bytes from the bit-writer buffer to the stream. @@ -258,7 +258,7 @@ public WebpVp8X EncodeHeader(Image image, Stream stream, bool ha if (hasAnimation) { WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); - BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount); } return vp8x; @@ -315,8 +315,8 @@ public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrame (uint)bounds.Width, (uint)bounds.Height, frameMetadata.FrameDelay, - frameMetadata.BlendMethod, - frameMetadata.DisposalMethod) + frameMetadata.BlendMode, + frameMetadata.DisposalMode) .WriteHeaderTo(stream); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3ad72f7d00..d22d357fe3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -495,8 +495,8 @@ private bool Encode(Stream stream, ImageFrame frame, Rectangle b (uint)bounds.Width, (uint)bounds.Height, frameMetadata.FrameDelay, - frameMetadata.BlendMethod, - frameMetadata.DisposalMethod) + frameMetadata.BlendMode, + frameMetadata.DisposalMode) .WriteHeaderTo(stream); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 72405e480e..bfaaa831e1 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -220,8 +220,8 @@ private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData fram { WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); frameMetadata.FrameDelay = frameData.Duration; - frameMetadata.BlendMethod = frameData.BlendingMethod; - frameMetadata.DisposalMethod = frameData.DisposalMethod; + frameMetadata.BlendMode = frameData.BlendingMethod; + frameMetadata.DisposalMode = frameData.DisposalMethod; } /// diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bc93df3a5b..226719c792 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image encoder for writing an image to a stream in the Webp format. /// -public sealed class WebpEncoder : ImageEncoder +public sealed class WebpEncoder : AnimatedImageEncoder { /// /// Gets the webp file format used. Either lossless or lossy. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 733801d636..37d2ae0a19 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -78,6 +77,19 @@ internal sealed class WebpEncoderCore /// private readonly WebpFileFormatType? fileFormat; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + /// /// The global configuration. /// @@ -103,6 +115,8 @@ public WebpEncoderCore(WebpEncoder encoder, Configuration configuration) this.skipMetadata = encoder.SkipMetadata; this.nearLossless = encoder.NearLossless; this.nearLosslessQuality = encoder.NearLosslessQuality; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; } /// @@ -147,7 +161,7 @@ public void Encode(Image image, Stream stream, CancellationToken long initialPosition = stream.Position; bool hasAlpha = false; - WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation); + WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount); // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; @@ -156,7 +170,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (hasAnimation) { - FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. @@ -164,12 +178,20 @@ public void Encode(Image image, Stream stream, CancellationToken for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; frameMetadata = currentFrame.Metadata.GetWebpMetadata(); - bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -178,7 +200,7 @@ public void Encode(Image image, Stream stream, CancellationToken currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend, ClampingMode.Even); @@ -197,7 +219,7 @@ public void Encode(Image image, Stream stream, CancellationToken hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; + previousDisposal = frameMetadata.DisposalMode; } } @@ -229,7 +251,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); - FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); @@ -239,12 +261,20 @@ public void Encode(Image image, Stream stream, CancellationToken for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; frameMetadata = currentFrame.Metadata.GetWebpMetadata(); - bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -253,7 +283,7 @@ public void Encode(Image image, Stream stream, CancellationToken currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend, ClampingMode.Even); @@ -273,7 +303,7 @@ public void Encode(Image image, Stream stream, CancellationToken hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; + previousDisposal = frameMetadata.DisposalMode; } encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index 3865f9837f..3f976a6401 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -24,19 +24,21 @@ public WebpFrameMetadata() private WebpFrameMetadata(WebpFrameMetadata other) { this.FrameDelay = other.FrameDelay; - this.DisposalMethod = other.DisposalMethod; - this.BlendMethod = other.BlendMethod; + this.DisposalMode = other.DisposalMode; + this.BlendMode = other.BlendMode; } /// - /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels + /// of the previous canvas. /// - public FrameBlendMode BlendMethod { get; set; } + public FrameBlendMode BlendMode { get; set; } /// - /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// Gets or sets how the current frame is to be treated after it has been displayed + /// (before rendering the next frame) on the canvas. /// - public FrameDisposalMode DisposalMethod { get; set; } + public FrameDisposalMode DisposalMode { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, @@ -49,8 +51,8 @@ public static WebpFrameMetadata FromFormatConnectingFrameMetadata(FormatConnecti => new() { FrameDelay = (uint)metadata.Duration.TotalMilliseconds, - BlendMethod = metadata.BlendMode, - DisposalMethod = GetMode(metadata.DisposalMode) + BlendMode = metadata.BlendMode, + DisposalMode = GetMode(metadata.DisposalMode) }; /// @@ -59,8 +61,8 @@ public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() { ColorTableMode = FrameColorTableMode.Global, Duration = TimeSpan.FromMilliseconds(this.FrameDelay), - DisposalMode = this.DisposalMethod, - BlendMode = this.BlendMethod, + DisposalMode = this.DisposalMode, + BlendMode = this.BlendMode, }; /// diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index d6b35d003f..a88884a407 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,18 +18,18 @@ - - + + - - - - - + + + + + - + diff --git a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs index 2ebab5e001..7a3fc0b395 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs @@ -29,22 +29,22 @@ public void SetupData() [Benchmark(Baseline = true, Description = "ImageMagick Tga")] public int TgaImageMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.Tga }; - using var image = new MagickImage(new MemoryStream(this.data), settings); - return image.Width; + MagickReadSettings settings = new() { Format = MagickFormat.Tga }; + using MagickImage image = new(new MemoryStream(this.data), settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Tga")] public int TgaImageSharp() { - using var image = Image.Load(this.data); + using Image image = Image.Load(this.data); return image.Width; } [Benchmark(Description = "Pfim Tga")] public int TgaPfim() { - using var image = Targa.Create(this.data, this.pfimConfig); + using Targa image = Targa.Create(this.data, this.pfimConfig); return image.Width; } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs index 6c71a62b50..4d6252c2b6 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs @@ -44,34 +44,35 @@ public void ReadImages() [Benchmark(Description = "Magick Lossy Webp")] public int WebpLossyMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.WebP }; - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = new MagickImage(memoryStream, settings); - return image.Width; + MagickReadSettings settings = new() { Format = MagickFormat.WebP }; + using MemoryStream memoryStream = new(this.webpLossyBytes); + using MagickImage image = new(memoryStream, settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Lossy Webp")] public int WebpLossy() { - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = Image.Load(memoryStream); + using MemoryStream memoryStream = new(this.webpLossyBytes); + using Image image = Image.Load(memoryStream); return image.Height; } [Benchmark(Description = "Magick Lossless Webp")] public int WebpLosslessMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.WebP }; - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = new MagickImage(memoryStream, settings); - return image.Width; + MagickReadSettings settings = new() + { Format = MagickFormat.WebP }; + using MemoryStream memoryStream = new(this.webpLossyBytes); + using MagickImage image = new(memoryStream, settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Lossless Webp")] public int WebpLossless() { - using var memoryStream = new MemoryStream(this.webpLosslessBytes); - using var image = Image.Load(memoryStream); + using MemoryStream memoryStream = new(this.webpLosslessBytes); + using Image image = Image.Load(memoryStream); return image.Height; } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs index 5be46a2220..d78640549d 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs @@ -13,7 +13,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs; [MarkdownExporter] [HtmlExporter] [Config(typeof(Config.Short))] +#pragma warning disable CA1001 // Types that own disposable fields should be disposable public class EncodeWebp +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { private MagickImage webpMagick; private Image webp; @@ -52,10 +54,7 @@ public void MagickWebpLossy() AlphaCompression = WebPAlphaCompression.None, FilterStrength = 60, SnsStrength = 50, - Pass = 1, - - // 100 means off. - NearLossless = 100 + Pass = 1 }; this.webpMagick.Quality = 75; @@ -85,9 +84,6 @@ public void MagickWebpLossless() { Lossless = true, Method = 4, - - // 100 means off. - NearLossless = 100 }; this.webpMagick.Quality = 75; diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index a705b24b28..37a991248c 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -41,8 +41,8 @@ - - + + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index a06784f1b1..62f41fe912 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -19,6 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave; +[Flags] public enum JpegKind { Baseline = 1, @@ -30,7 +31,7 @@ public class LoadResizeSaveStressRunner { private const int Quality = 75; - // Set the quality for ImagSharp + // Set the quality for ImageSharp private readonly JpegEncoder imageSharpJpegEncoder = new() { Quality = Quality }; private readonly ImageCodecInfo systemDrawingJpegCodec = ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid); @@ -126,7 +127,7 @@ public Task ForEachImageParallelAsync(Func action) : Environment.ProcessorCount; int partitionSize = (int)Math.Ceiling((double)this.Images.Length / maxDegreeOfParallelism); - List tasks = new(); + List tasks = []; for (int i = 0; i < this.Images.Length; i += partitionSize) { int end = Math.Min(i + partitionSize, this.Images.Length); @@ -176,13 +177,13 @@ private string OutputPath(string inputPath, [CallerMemberName] string postfix = public void SystemDrawingResize(string input) { - using var image = SystemDrawingImage.FromFile(input, true); + using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true); this.LogImageProcessed(image.Width, image.Height); - (int Width, int Height) scaled = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); - var resized = new Bitmap(scaled.Width, scaled.Height); - using var graphics = Graphics.FromImage(resized); - using var attributes = new ImageAttributes(); + (int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); + Bitmap resized = new(width, height); + using Graphics graphics = Graphics.FromImage(resized); + using ImageAttributes attributes = new(); attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.CompositingMode = CompositingMode.SourceCopy; @@ -191,8 +192,8 @@ public void SystemDrawingResize(string input) graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes); // Save the results - using var encoderParams = new EncoderParameters(1); - using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality); + using EncoderParameters encoderParams = new(1); + using EncoderParameter qualityParam = new(Encoder.Quality, (long)Quality); encoderParams.Param[0] = qualityParam; resized.Save(this.OutputPath(input), this.systemDrawingJpegCodec, encoderParams); } @@ -223,7 +224,7 @@ public void ImageSharpResize(string input) public async Task ImageSharpResizeAsync(string input) { - using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); + await using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); // Resize it to fit a 150x150 square. DecoderOptions options = new() @@ -246,11 +247,11 @@ public async Task ImageSharpResizeAsync(string input) public void MagickResize(string input) { - using var image = new MagickImage(input); - this.LogImageProcessed(image.Width, image.Height); + using MagickImage image = new(input); + this.LogImageProcessed((int)image.Width, (int)image.Height); // Resize it to fit a 150x150 square - image.Resize(this.ThumbnailSize, this.ThumbnailSize); + image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); // Reduce the size of the file image.Strip(); @@ -264,7 +265,7 @@ public void MagickResize(string input) public void MagicScalerResize(string input) { - var settings = new ProcessImageSettings() + ProcessImageSettings settings = new() { Width = this.ThumbnailSize, Height = this.ThumbnailSize, @@ -273,19 +274,19 @@ public void MagicScalerResize(string input) }; // TODO: Is there a way to capture input dimensions for IncreaseTotalMegapixels? - using var output = new FileStream(this.OutputPath(input), FileMode.Create); + using FileStream output = new(this.OutputPath(input), FileMode.Create); MagicImageProcessor.ProcessImage(input, output, settings); } public void SkiaCanvasResize(string input) { - using var original = SKBitmap.Decode(input); + using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); - using var surface = SKSurface.Create(new SKImageInfo(scaled.Width, scaled.Height, original.ColorType, original.AlphaType)); - using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High }; + (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType)); + using SKPaint paint = new() { FilterQuality = SKFilterQuality.High }; SKCanvas canvas = surface.Canvas; - canvas.Scale((float)scaled.Width / original.Width); + canvas.Scale((float)width / original.Width); canvas.DrawBitmap(original, 0, 0, paint); canvas.Flush(); @@ -297,16 +298,16 @@ public void SkiaCanvasResize(string input) public void SkiaBitmapResize(string input) { - using var original = SKBitmap.Decode(input); + using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); - using var resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); + (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); if (resized == null) { return; } - using var image = SKImage.FromBitmap(resized); + using SKImage image = SKImage.FromBitmap(resized); using FileStream output = File.OpenWrite(this.OutputPath(input)); image.Encode(SKEncodedImageFormat.Jpeg, Quality) .SaveTo(output); @@ -314,21 +315,21 @@ public void SkiaBitmapResize(string input) public void SkiaBitmapDecodeToTargetSize(string input) { - using var codec = SKCodec.Create(input); + using SKCodec codec = SKCodec.Create(input); SKImageInfo info = codec.Info; this.LogImageProcessed(info.Width, info.Height); - (int Width, int Height) scaled = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); - SKSizeI supportedScale = codec.GetScaledDimensions((float)scaled.Width / info.Width); + (int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); + SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width); - using var original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); - using SKBitmap resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); + using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); + using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); if (resized == null) { return; } - using var image = SKImage.FromBitmap(resized); + using SKImage image = SKImage.FromBitmap(resized); using FileStream output = File.OpenWrite(this.OutputPath(input, nameof(this.SkiaBitmapDecodeToTargetSize))); image.Encode(SKEncodedImageFormat.Jpeg, Quality) @@ -338,7 +339,7 @@ public void SkiaBitmapDecodeToTargetSize(string input) public void NetVipsResize(string input) { // Thumbnail to fit a 150x150 square - using var thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize); + using NetVipsImage thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize); // Save the results thumb.Jpegsave(this.OutputPath(input), q: Quality, keep: NetVips.Enums.ForeignKeep.None); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 77ac51e8a1..c08db84eb6 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -334,7 +334,7 @@ public void Encode_AnimatedFormatTransform_FromWebp(TestImageProvider(TestImageProvider(TestImageProvider(TestImageProvider(TestImageProvider(TestImageProvider provider Image imageFromMagick; using (Stream stream = LoadAsStream(provider)) { - var magickImage = new MagickImage(stream); + using MagickImage magickImage = new(stream); // Apply Auto Level using the Grey (BT.709) channel. magickImage.AutoLevel(Channels.Gray); imageFromMagick = ConvertImageFromMagick(magickImage); } - using (Image image = provider.GetImage()) + using Image image = provider.GetImage(); + HistogramEqualizationOptions options = new() { - var options = new HistogramEqualizationOptions - { - Method = HistogramEqualizationMethod.AutoLevel, - LuminanceLevels = 256, - SyncChannels = true - }; - image.Mutate(x => x.HistogramEqualization(options)); - image.DebugSave(provider); - ExactImageComparer.Instance.CompareImages(imageFromMagick, image); - } + Method = HistogramEqualizationMethod.AutoLevel, + LuminanceLevels = 256, + SyncChannels = true + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + ExactImageComparer.Instance.CompareImages(imageFromMagick, image); + + imageFromMagick.Dispose(); } - private Stream LoadAsStream(TestImageProvider provider) + private static FileStream LoadAsStream(TestImageProvider provider) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToMagick() works only with file providers!"); - } + string path = TestImageProvider.GetFilePathOrNull(provider) + ?? throw new InvalidOperationException("CompareToMagick() works only with file providers!"); - var testFile = TestFile.Create(path); + TestFile testFile = TestFile.Create(path); return new FileStream(testFile.FullPath, FileMode.Open); } - private Image ConvertImageFromMagick(MagickImage magickImage) + private static Image ConvertImageFromMagick(MagickImage magickImage) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { Configuration configuration = Configuration.Default.Clone(); configuration.PreferContiguousImageBuffers = true; - var result = new Image(configuration, magickImage.Width, magickImage.Height); + Image result = new(configuration, (int)magickImage.Width, (int)magickImage.Height); Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs index f5c70b0885..05f65cfbbc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs @@ -15,13 +15,10 @@ public static void CompareWithReferenceDecoder( float compareTolerance = 0.01f) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - } + string path = TestImageProvider.GetFilePathOrNull(provider) + ?? throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - var testFile = TestFile.Create(path); + TestFile testFile = TestFile.Create(path); using Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); if (useExactComparer) { @@ -38,25 +35,23 @@ public static Image DecodeWithMagick(FileInfo fileInfo) { Configuration configuration = Configuration.Default.Clone(); configuration.PreferContiguousImageBuffers = true; - using (var magickImage = new MagickImage(fileInfo)) - { - magickImage.AutoOrient(); - var result = new Image(configuration, magickImage.Width, magickImage.Height); + using MagickImage magickImage = new(fileInfo); + magickImage.AutoOrient(); + Image result = new(configuration, (int)magickImage.Width, (int)magickImage.Height); - Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); + Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); - using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels.Span, - resultPixels.Length); - } + using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - return result; + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels.Span, + resultPixels.Length); } + + return result; } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index f96dc19ee0..3cb0381950 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -58,7 +58,7 @@ protected override Image Decode(DecoderOptions options, Stream s MagickReadSettings settings = new() { - FrameCount = (int)options.MaxFrames + FrameCount = options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines); @@ -67,7 +67,7 @@ protected override Image Decode(DecoderOptions options, Stream s List> framesList = []; foreach (IMagickImage magicFrame in magickImageCollection) { - ImageFrame frame = new(configuration, magicFrame.Width, magicFrame.Height); + ImageFrame frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height); framesList.Add(frame); MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; From 45965112589857ff8891740e4b80b1656e0c284b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2024 21:39:34 +1000 Subject: [PATCH 2/3] Clean up background index check --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 757b8320dc..0ed7e8c98d 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -156,12 +156,7 @@ public void Encode(Image image, Stream stream, CancellationToken frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); } - byte backgroundIndex; - if (this.backgroundColor.HasValue) - { - backgroundIndex = GetBackgroundIndex(quantized, this.backgroundColor.Value); - } - else + if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex)) { backgroundIndex = derivedTransparencyIndex >= 0 ? frameMetadata.TransparencyIndex @@ -510,15 +505,19 @@ private static int GetTransparentIndex(IndexedImageFrame? quanti /// /// The current quantized frame. /// The background color to match. + /// The index in the palette of the background color. /// The pixel format. - /// The . - private static byte GetBackgroundIndex(IndexedImageFrame? quantized, Color background) + /// The . + private static bool TryGetBackgroundIndex( + IndexedImageFrame? quantized, + Color? background, + out byte index) where TPixel : unmanaged, IPixel { - int index = -1; - if (quantized != null) + int match = -1; + if (quantized != null && background.HasValue) { - TPixel backgroundPixel = background.ToPixel(); + TPixel backgroundPixel = background.Value.ToPixel(); ReadOnlySpan palette = quantized.Palette.Span; for (int i = 0; i < palette.Length; i++) { @@ -527,12 +526,19 @@ private static byte GetBackgroundIndex(IndexedImageFrame? quanti continue; } - index = i; + match = i; break; } } - return (byte)Numerics.Clamp(index, 0, 255); + if (match >= 0) + { + index = (byte)Numerics.Clamp(match, 0, 255); + return true; + } + + index = 0; + return false; } /// From f4923598bb784a33daf8a979c460d4f3cf7d9b1b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 21 Nov 2024 20:18:54 +1000 Subject: [PATCH 3/3] Limit Magick.NET version for now --- tests/Directory.Build.targets | 7 ++++++- .../LoadResizeSave/LoadResizeSaveStressRunner.cs | 4 ++-- .../ReferenceCodecs/MagickReferenceDecoder.cs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index a88884a407..e20717cdd3 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,7 +19,12 @@ - + + + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 62f41fe912..e7d240acd3 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -248,10 +248,10 @@ public async Task ImageSharpResizeAsync(string input) public void MagickResize(string input) { using MagickImage image = new(input); - this.LogImageProcessed((int)image.Width, (int)image.Height); + this.LogImageProcessed(image.Width, image.Height); // Resize it to fit a 150x150 square - image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); + image.Resize(this.ThumbnailSize, this.ThumbnailSize); // Reduce the size of the file image.Strip(); diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 3cb0381950..74015a4eff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -58,7 +58,7 @@ protected override Image Decode(DecoderOptions options, Stream s MagickReadSettings settings = new() { - FrameCount = options.MaxFrames + FrameCount = (int)options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines);