Browse Source

Simplify GifEncoderCore, seal palette quantizers

pull/2894/head
James Jackson-South 10 months ago
parent
commit
b989843f18
  1. 216
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 2
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  3. 2
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs

216
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
@ -19,6 +18,8 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// </summary> /// </summary>
internal sealed class GifEncoderCore internal sealed class GifEncoderCore
{ {
private readonly GifEncoder encoder;
/// <summary> /// <summary>
/// Used for allocating memory during processing operations. /// Used for allocating memory during processing operations.
/// </summary> /// </summary>
@ -34,21 +35,6 @@ internal sealed class GifEncoderCore
/// </summary> /// </summary>
private readonly bool skipMetadata; private readonly bool skipMetadata;
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// The fallback quantizer to use when no quantizer is provided.
/// </summary>
private static readonly IQuantizer FallbackQuantizer = KnownQuantizers.Octree;
/// <summary>
/// Whether the quantizer was supplied via options.
/// </summary>
private readonly bool hasQuantizer;
/// <summary> /// <summary>
/// The color table mode: Global or local. /// The color table mode: Global or local.
/// </summary> /// </summary>
@ -86,9 +72,8 @@ internal sealed class GifEncoderCore
{ {
this.configuration = configuration; this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.skipMetadata = encoder.SkipMetadata; this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode; this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor; this.backgroundColor = encoder.BackgroundColor;
@ -124,64 +109,64 @@ internal sealed class GifEncoderCore
// Quantize the first image frame returning a palette. // Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null; IndexedImageFrame<TPixel>? 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. // Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{ {
// We avoid dithering by default to preserve the original colors.
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256) 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 else
{ {
this.quantizer = FallbackQuantizer; globalQuantizer = new OctreeQuantizer(options);
} }
} }
else 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. // Quantize the first frame.
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options)) IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
{
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame; ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame;
if (useGlobalTableForFirstFrame) if (useGlobalTableForFirstFrame)
{
using IQuantizer<TPixel> firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
if (useGlobalTable)
{ {
if (useGlobalTable) firstFrameQuantizer.BuildPalette(strategy, image);
{
frameQuantizer.BuildPalette(strategy, image);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
else
{
frameQuantizer.BuildPalette(strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
}
} }
else else
{ {
quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( firstFrameQuantizer.BuildPalette(strategy, encodingFrame);
encodingFrame,
options,
encodingFrame.Bounds,
frameMetadata,
true,
default,
false,
frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1,
Color.Transparent);
} }
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. // 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. // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
this.EncodeAdditionalFrames( if (image.Frames.Count > 1)
stream, {
image, using PaletteQuantizer<TPixel> globalFrameQuantizer = new(this.configuration, globalQuantizer.Options, quantized.Palette.ToArray());
options, this.EncodeAdditionalFrames(
globalPalette, stream,
derivedTransparencyIndex, image,
frameMetadata.DisposalMode, globalQuantizer,
cancellationToken); globalFrameQuantizer,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
}
} }
finally finally
{ {
@ -264,70 +253,43 @@ internal sealed class GifEncoderCore
private void EncodeAdditionalFrames<TPixel>( private void EncodeAdditionalFrames<TPixel>(
Stream stream, Stream stream,
Image<TPixel> image, Image<TPixel> image,
QuantizerOptions options, IQuantizer globalQuantizer,
ReadOnlyMemory<TPixel> globalPalette, PaletteQuantizer<TPixel> globalFrameQuantizer,
int globalTransparencyIndex, int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode, FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (image.Frames.Count == 1)
{
return;
}
PaletteQuantizer<TPixel> globalPaletteQuantizer = default;
bool hasGlobalPaletteQuantizer = false;
// Store the first frame as a reference for de-duplication comparison. // Store the first frame as a reference for de-duplication comparison.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size); using ImageFrame<TPixel> 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. // Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasGlobalPaletteQuantizer && i > 0) this.EncodeAdditionalFrame(
{ stream,
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. previousFrame,
// This allows a reduction of memory usage across multi-frame gifs using a global palette currentFrame,
// and also allows use to reuse the cache from previous runs. nextFrame,
globalPaletteQuantizer = new(this.configuration, options, globalPalette); encodingFrame,
hasGlobalPaletteQuantizer = true; globalQuantizer,
} globalFrameQuantizer,
useLocal,
gifMetadata,
previousDisposalMode);
this.EncodeAdditionalFrame( previousFrame = currentFrame;
stream, previousDisposalMode = gifMetadata.DisposalMode;
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
options,
useLocal,
gifMetadata,
globalPaletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
}
finally
{
if (hasGlobalPaletteQuantizer)
{
globalPaletteQuantizer.Dispose();
}
} }
} }
@ -363,10 +325,10 @@ internal sealed class GifEncoderCore
ImageFrame<TPixel> currentFrame, ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame, ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> encodingFrame, ImageFrame<TPixel> encodingFrame,
QuantizerOptions options, IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
bool useLocal, bool useLocal,
GifFrameMetadata metadata, GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
FrameDisposalMode previousDisposalMode) FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -393,13 +355,13 @@ internal sealed class GifEncoderCore
background, background,
true); true);
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( using IndexedImageFrame<TPixel> quantized = this.QuantizeFrameAndUpdateMetadata(
encodingFrame, encodingFrame,
options, globalQuantizer,
globalFrameQuantizer,
bounds, bounds,
metadata, metadata,
useLocal, useLocal,
globalPaletteQuantizer,
difference, difference,
transparencyIndex, transparencyIndex,
background); background);
@ -418,13 +380,13 @@ internal sealed class GifEncoderCore
this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex); this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
} }
private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixel>( private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(
ImageFrame<TPixel> encodingFrame, ImageFrame<TPixel> encodingFrame,
QuantizerOptions options, IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
Rectangle bounds, Rectangle bounds,
GifFrameMetadata metadata, GifFrameMetadata metadata,
bool useLocal, bool useLocal,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
bool hasDuplicates, bool hasDuplicates,
int transparencyIndex, int transparencyIndex,
Color transparentColor) Color transparentColor)
@ -451,20 +413,19 @@ internal sealed class GifEncoderCore
transparencyIndex = palette.Length; transparencyIndex = palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex); metadata.TransparencyIndex = ClampIndex(transparencyIndex);
QuantizerOptions paletteOptions = options.DeepClone(o => QuantizerOptions options = globalQuantizer.Options.DeepClone(o =>
{ {
o.MaxColors = palette.Length; o.MaxColors = palette.Length;
o.Dither = null; o.Dither = null;
}); });
PaletteQuantizer quantizer = new(palette, paletteOptions, transparencyIndex, transparentColor); PaletteQuantizer quantizer = new(palette, options, transparencyIndex, transparentColor);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options); using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
} }
else else
{ {
// We must quantize the frame to generate a local color table. // We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
// The transparency index derived by the quantizer will differ from the index // The transparency index derived by the quantizer will differ from the index
@ -476,7 +437,7 @@ internal sealed class GifEncoderCore
else else
{ {
// Just use the local palette. // Just use the local palette.
QuantizerOptions paletteOptions = options.DeepClone(o => QuantizerOptions paletteOptions = globalQuantizer.Options.DeepClone(o =>
{ {
o.MaxColors = palette.Length; o.MaxColors = palette.Length;
o.Dither = null; o.Dither = null;
@ -489,8 +450,7 @@ internal sealed class GifEncoderCore
else else
{ {
// We must quantize the frame to generate a local color table. // We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
// The transparency index derived by the quantizer might differ from the index // The transparency index derived by the quantizer might differ from the index
@ -520,12 +480,12 @@ internal sealed class GifEncoderCore
if (hasDuplicates && !metadata.HasTransparency) if (hasDuplicates && !metadata.HasTransparency)
{ {
metadata.HasTransparency = true; metadata.HasTransparency = true;
transparencyIndex = globalPaletteQuantizer.Palette.Length; transparencyIndex = globalFrameQuantizer.Palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex); metadata.TransparencyIndex = ClampIndex(transparencyIndex);
} }
globalPaletteQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>()); globalFrameQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>());
quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds); quantized = globalFrameQuantizer.QuantizeFrame(encodingFrame, bounds);
} }
return quantized; return quantized;

2
src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// <summary> /// <summary>
/// A palette quantizer consisting of web safe colors as defined in the CSS Color Module Level 4. /// A palette quantizer consisting of web safe colors as defined in the CSS Color Module Level 4.
/// </summary> /// </summary>
public class WebSafePaletteQuantizer : PaletteQuantizer public sealed class WebSafePaletteQuantizer : PaletteQuantizer
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.

2
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. /// 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 <see href="https://www.c82.net/werner"/> /// The hex codes were collected and defined by Nicholas Rougeux <see href="https://www.c82.net/werner"/>
/// </summary> /// </summary>
public class WernerPaletteQuantizer : PaletteQuantizer public sealed class WernerPaletteQuantizer : PaletteQuantizer
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.

Loading…
Cancel
Save