From b989843f181eeae341d28268ebc85aabc352df7d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Apr 2025 00:40:51 +1000 Subject: [PATCH] Simplify GifEncoderCore, seal palette quantizers --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 216 +++++++----------- .../Quantization/WebSafePaletteQuantizer.cs | 2 +- .../Quantization/WernerPaletteQuantizer.cs | 2 +- 3 files changed, 90 insertions(+), 130 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c0d74e03e..43af476f2 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif; @@ -19,6 +18,8 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// internal sealed class GifEncoderCore { + private readonly GifEncoder encoder; + /// /// Used for allocating memory during processing operations. /// @@ -34,21 +35,6 @@ internal sealed class GifEncoderCore /// private readonly bool skipMetadata; - /// - /// The quantizer used to generate the color palette. - /// - private IQuantizer? quantizer; - - /// - /// The fallback quantizer to use when no quantizer is provided. - /// - private static readonly IQuantizer FallbackQuantizer = KnownQuantizers.Octree; - - /// - /// Whether the quantizer was supplied via options. - /// - private readonly bool hasQuantizer; - /// /// The color table mode: Global or local. /// @@ -86,9 +72,8 @@ internal sealed class GifEncoderCore { this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; + this.encoder = encoder; this.skipMetadata = encoder.SkipMetadata; - this.quantizer = encoder.Quantizer; - this.hasQuantizer = encoder.Quantizer is not null; this.colorTableMode = encoder.ColorTableMode; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.backgroundColor = encoder.BackgroundColor; @@ -124,64 +109,64 @@ internal sealed class GifEncoderCore // Quantize the first image frame returning a palette. IndexedImageFrame? quantized = null; - if (this.quantizer is null) + IQuantizer? globalQuantizer = this.encoder.Quantizer; + TransparentColorMode mode = this.transparentColorMode; + + // Create a new quantizer options instance augmenting the transparent color mode to match the encoder. + QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new()).DeepClone(o => o.TransparentColorMode = mode); + + if (globalQuantizer is null) { // Is this a gif with color information. If so use that, otherwise use octree. if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) { - // We avoid dithering by default to preserve the original colors. int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256) { - this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }); + // We avoid dithering by default to preserve the original colors. + globalQuantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, options.DeepClone(o => o.Dither = null)); } else { - this.quantizer = FallbackQuantizer; + globalQuantizer = new OctreeQuantizer(options); } } else { - this.quantizer = FallbackQuantizer; + globalQuantizer = new OctreeQuantizer(options); } } - // Create a new quantizer options instance augmenting the transparent color mode to match the encoder. - TransparentColorMode mode = this.transparentColorMode; - QuantizerOptions options = this.quantizer.Options.DeepClone(o => o.TransparentColorMode = mode); - // Quantize the first frame. - using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, options)) - { - IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; + IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; - ImageFrame encodingFrame = image.Frames.RootFrame; - if (useGlobalTableForFirstFrame) + ImageFrame encodingFrame = image.Frames.RootFrame; + if (useGlobalTableForFirstFrame) + { + using IQuantizer firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration, options); + if (useGlobalTable) { - if (useGlobalTable) - { - frameQuantizer.BuildPalette(strategy, image); - quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds); - } - else - { - frameQuantizer.BuildPalette(strategy, encodingFrame); - quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); - } + firstFrameQuantizer.BuildPalette(strategy, image); } else { - quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( - encodingFrame, - options, - encodingFrame.Bounds, - frameMetadata, - true, - default, - false, - frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1, - Color.Transparent); + firstFrameQuantizer.BuildPalette(strategy, encodingFrame); } + + quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + } + else + { + quantized = this.QuantizeFrameAndUpdateMetadata( + encodingFrame, + globalQuantizer, + default, + encodingFrame.Bounds, + frameMetadata, + true, + false, + frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1, + Color.Transparent); } // Write the header. @@ -231,14 +216,18 @@ internal sealed class GifEncoderCore // 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, - options, - globalPalette, - derivedTransparencyIndex, - frameMetadata.DisposalMode, - cancellationToken); + if (image.Frames.Count > 1) + { + using PaletteQuantizer globalFrameQuantizer = new(this.configuration, globalQuantizer.Options, quantized.Palette.ToArray()); + this.EncodeAdditionalFrames( + stream, + image, + globalQuantizer, + globalFrameQuantizer, + derivedTransparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); + } } finally { @@ -264,70 +253,43 @@ internal sealed class GifEncoderCore private void EncodeAdditionalFrames( Stream stream, Image image, - QuantizerOptions options, - ReadOnlyMemory globalPalette, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, int globalTransparencyIndex, FrameDisposalMode previousDisposalMode, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - if (image.Frames.Count == 1) - { - return; - } - - PaletteQuantizer globalPaletteQuantizer = default; - bool hasGlobalPaletteQuantizer = false; - // Store the first frame as a reference for de-duplication comparison. ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size); - try + for (int i = 1; i < image.Frames.Count; i++) { - for (int i = 1; i < image.Frames.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - // 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 && !hasGlobalPaletteQuantizer && 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. - globalPaletteQuantizer = new(this.configuration, options, globalPalette); - hasGlobalPaletteQuantizer = true; - } + this.EncodeAdditionalFrame( + stream, + previousFrame, + currentFrame, + nextFrame, + encodingFrame, + globalQuantizer, + globalFrameQuantizer, + useLocal, + gifMetadata, + previousDisposalMode); - this.EncodeAdditionalFrame( - stream, - previousFrame, - currentFrame, - nextFrame, - encodingFrame, - options, - useLocal, - gifMetadata, - globalPaletteQuantizer, - previousDisposalMode); - - previousFrame = currentFrame; - previousDisposalMode = gifMetadata.DisposalMode; - } - } - finally - { - if (hasGlobalPaletteQuantizer) - { - globalPaletteQuantizer.Dispose(); - } + previousFrame = currentFrame; + previousDisposalMode = gifMetadata.DisposalMode; } } @@ -363,10 +325,10 @@ internal sealed class GifEncoderCore ImageFrame currentFrame, ImageFrame? nextFrame, ImageFrame encodingFrame, - QuantizerOptions options, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, bool useLocal, GifFrameMetadata metadata, - PaletteQuantizer globalPaletteQuantizer, FrameDisposalMode previousDisposalMode) where TPixel : unmanaged, IPixel { @@ -393,13 +355,13 @@ internal sealed class GifEncoderCore background, true); - using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( + using IndexedImageFrame quantized = this.QuantizeFrameAndUpdateMetadata( encodingFrame, - options, + globalQuantizer, + globalFrameQuantizer, bounds, metadata, useLocal, - globalPaletteQuantizer, difference, transparencyIndex, background); @@ -418,13 +380,13 @@ internal sealed class GifEncoderCore this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex); } - private IndexedImageFrame QuantizeAdditionalFrameAndUpdateMetadata( + private IndexedImageFrame QuantizeFrameAndUpdateMetadata( ImageFrame encodingFrame, - QuantizerOptions options, + IQuantizer globalQuantizer, + PaletteQuantizer globalFrameQuantizer, Rectangle bounds, GifFrameMetadata metadata, bool useLocal, - PaletteQuantizer globalPaletteQuantizer, bool hasDuplicates, int transparencyIndex, Color transparentColor) @@ -451,20 +413,19 @@ internal sealed class GifEncoderCore transparencyIndex = palette.Length; metadata.TransparencyIndex = ClampIndex(transparencyIndex); - QuantizerOptions paletteOptions = options.DeepClone(o => + QuantizerOptions options = globalQuantizer.Options.DeepClone(o => { o.MaxColors = palette.Length; o.Dither = null; }); - PaletteQuantizer quantizer = new(palette, paletteOptions, transparencyIndex, transparentColor); - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + PaletteQuantizer quantizer = new(palette, options, transparencyIndex, transparentColor); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); } else { // We must quantize the frame to generate a local color table. - IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, options); + using IQuantizer frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); // The transparency index derived by the quantizer will differ from the index @@ -476,7 +437,7 @@ internal sealed class GifEncoderCore else { // Just use the local palette. - QuantizerOptions paletteOptions = options.DeepClone(o => + QuantizerOptions paletteOptions = globalQuantizer.Options.DeepClone(o => { o.MaxColors = palette.Length; o.Dither = null; @@ -489,8 +450,7 @@ internal sealed class GifEncoderCore else { // We must quantize the frame to generate a local color table. - IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, options); + using IQuantizer frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); // The transparency index derived by the quantizer might differ from the index @@ -520,12 +480,12 @@ internal sealed class GifEncoderCore if (hasDuplicates && !metadata.HasTransparency) { metadata.HasTransparency = true; - transparencyIndex = globalPaletteQuantizer.Palette.Length; + transparencyIndex = globalFrameQuantizer.Palette.Length; metadata.TransparencyIndex = ClampIndex(transparencyIndex); } - globalPaletteQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel()); - quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds); + globalFrameQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel()); + quantized = globalFrameQuantizer.QuantizeFrame(encodingFrame, bounds); } return quantized; diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index 604cae668..fa1763367 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// /// A palette quantizer consisting of web safe colors as defined in the CSS Color Module Level 4. /// -public class WebSafePaletteQuantizer : PaletteQuantizer +public sealed class WebSafePaletteQuantizer : PaletteQuantizer { /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 023ee7f2e..cd7b80e81 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// A palette quantizer consisting of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821. /// The hex codes were collected and defined by Nicholas Rougeux /// -public class WernerPaletteQuantizer : PaletteQuantizer +public sealed class WernerPaletteQuantizer : PaletteQuantizer { /// /// Initializes a new instance of the class.