Browse Source

Merge pull request #2605 from SixLabors/js/fix-gif-edge-case

Handle dedup of local palette of 256 length
pull/2609/head
James Jackson-South 2 years ago
committed by GitHub
parent
commit
cb07e368dc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 10
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  3. 4
      src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs
  4. 4
      tests/ImageSharp.Tests/TestImages.cs
  5. 3
      tests/Images/Input/Gif/18-bit_RGB_Cube.gif

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

@ -380,18 +380,42 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlyMemory<Color> 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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
}
else
{

10
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -77,14 +77,20 @@ public static partial class MetadataExtensions
}
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
{

4
src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs

@ -71,7 +71,7 @@ internal class Vp8LBitReader : BitReaderBase
this.Eos = false;
ulong currentValue = 0;
System.Span<byte> dataSpan = this.Data.Memory.Span;
Span<byte> dataSpan = this.Data.Memory.Span;
for (int i = 0; i < 8; i++)
{
currentValue |= (ulong)dataSpan[i] << (8 * i);
@ -103,7 +103,7 @@ internal class Vp8LBitReader : BitReaderBase
}
ulong currentValue = 0;
System.Span<byte> dataSpan = this.Data.Memory.Span;
Span<byte> dataSpan = this.Data.Memory.Span;
for (int i = 0; i < length; i++)
{
currentValue |= (ulong)dataSpan[i] << (8 * i);

4
tests/ImageSharp.Tests/TestImages.cs

@ -487,6 +487,7 @@ public static class TestImages
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 TestImages
Issues.Issue2450_A,
Issues.Issue2450_B,
Issues.BadDescriptorWidth,
Issues.Issue1530
Issues.Issue1530,
Bit18RGBCube
};
}

3
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
Loading…
Cancel
Save