From f3ff11ea6b9f36c1cbdf9965c0366ef97a120906 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 7 Dec 2023 10:52:56 +1000 Subject: [PATCH] Handle dedup of local palette of 256 length --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 40 +++++++++++++++---- .../Formats/Gif/MetadataExtensions.cs | 10 ++++- .../Formats/Webp/BitReader/Vp8LBitReader.cs | 4 +- tests/ImageSharp.Tests/TestImages.cs | 4 +- tests/Images/Input/Gif/18-bit_RGB_Cube.gif | 3 ++ 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 tests/Images/Input/Gif/18-bit_RGB_Cube.gif diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index f0e1aafd7d..9988848d11 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -380,18 +380,42 @@ private IndexedImageFrame QuantizeAdditionalFrameAndUpdateMetadata palette = metadata.LocalColorTable.Value; - if (hasDuplicates && !metadata.HasTransparency) { - // A difference was captured but the metadata does not have transparency. + // Duplicates were captured but the metadata does not have transparency. metadata.HasTransparency = true; - transparencyIndex = palette.Length; - metadata.TransparencyIndex = ClampIndex(transparencyIndex); - } - PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex); - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); - quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + if (palette.Length < 256) + { + // We can use the existing palette and set the transparent index as the length. + // decoders will ignore this value. + transparencyIndex = palette.Length; + metadata.TransparencyIndex = ClampIndex(transparencyIndex); + + PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + } + else + { + // We must quantize the frame to generate a local color table. + IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree; + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + + // The transparency index derived by the quantizer will differ from the index + // within the metadata. We need to update the metadata to reflect this. + int derivedTransparencyIndex = GetTransparentIndex(quantized, null); + metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); + } + } + else + { + // Just use the local palette. + PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); + } } else { diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index 16f788e3db..42602f2c78 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -77,14 +77,20 @@ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata s } internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source) - => new() + { + // For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or + // has a local palette with 256 colors and is not transparent we should use 'Source'. + bool blendSource = source.DisposalMethod == GifDisposalMethod.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency); + + return new() { ColorTable = source.LocalColorTable, ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), DisposalMode = GetMode(source.DisposalMethod), - BlendMode = source.DisposalMethod == GifDisposalMethod.RestoreToBackground ? FrameBlendMode.Source : FrameBlendMode.Over, + BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over, }; + } private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch { diff --git a/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs index 659576cf11..6d3cab1514 100644 --- a/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs +++ b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs @@ -71,7 +71,7 @@ public Vp8LBitReader(IMemoryOwner data) this.Eos = false; ulong currentValue = 0; - System.Span dataSpan = this.Data.Memory.Span; + Span dataSpan = this.Data.Memory.Span; for (int i = 0; i < 8; i++) { currentValue |= (ulong)dataSpan[i] << (8 * i); @@ -103,7 +103,7 @@ public Vp8LBitReader(Stream inputStream, uint imageDataSize, MemoryAllocator mem } ulong currentValue = 0; - System.Span dataSpan = this.Data.Memory.Span; + Span dataSpan = this.Data.Memory.Span; for (int i = 0; i < length; i++) { currentValue |= (ulong)dataSpan[i] << (8 * i); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 7e862f7d4f..b628529028 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -487,6 +487,7 @@ public static class Gif public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif"; public const string MixedDisposal = "Gif/mixed-disposal.gif"; public const string M4nb = "Gif/m4nb.gif"; + public const string Bit18RGBCube = "Gif/18-bit_RGB_Cube.gif"; // Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite public const string ZeroSize = "Gif/image-zero-size.gif"; @@ -533,7 +534,8 @@ public static class Issues Issues.Issue2450_A, Issues.Issue2450_B, Issues.BadDescriptorWidth, - Issues.Issue1530 + Issues.Issue1530, + Bit18RGBCube }; } diff --git a/tests/Images/Input/Gif/18-bit_RGB_Cube.gif b/tests/Images/Input/Gif/18-bit_RGB_Cube.gif new file mode 100644 index 0000000000..5446661b41 --- /dev/null +++ b/tests/Images/Input/Gif/18-bit_RGB_Cube.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5148c8c192385966ec6ad5b3d35195a878355d71146a958e5ef02b7642a4e883 +size 4311986