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.