Browse Source

Merge pull request #3143 from SixLabors/js/fix-3142

Fix GIF transparency handling and dither
pull/2854/merge
James Jackson-South 2 weeks ago
committed by GitHub
parent
commit
58873574c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      src/ImageSharp/Color/Color.WernerPalette.cs
  2. 31
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 9
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  4. 10
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  5. 20
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  6. 4
      tests/ImageSharp.Tests/TestImages.cs
  7. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif
  8. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif
  9. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif
  10. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif
  11. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif
  12. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif
  13. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif
  14. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif
  15. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif
  16. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif
  17. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif
  18. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif
  19. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif
  20. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif
  21. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif
  22. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png
  23. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png
  24. 3
      tests/Images/Input/Gif/issues/issue_3142.gif

6
src/ImageSharp/Color/Color.WernerPalette.cs

@ -127,6 +127,10 @@ public partial struct Color
ParseHex("#8b7859"),
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
ParseHex("#453b32"),
// Werner does not define a transparent color, but we need to add one to
// make the palette work with the rest of the library.
Transparent
];
}

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

@ -361,7 +361,10 @@ internal sealed class GifEncoderCore
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
// Pixels matching the previous frame are replaced with the transparent placeholder.
// When the entire frame matches there is no captured difference, but every pixel is
// still a placeholder, so a transparent index is always required for additional frames.
(_, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
this.configuration,
previous,
@ -378,7 +381,7 @@ internal sealed class GifEncoderCore
bounds,
metadata,
useLocal,
difference,
true,
transparencyIndex,
background);
@ -403,7 +406,7 @@ internal sealed class GifEncoderCore
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
bool hasDuplicates,
bool requiresTransparency,
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
@ -417,9 +420,11 @@ internal sealed class GifEncoderCore
// 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)
if (requiresTransparency && !metadata.HasTransparency)
{
// Duplicates were captured but the metadata does not have transparency.
// The frame was de-duplicated against the previous frame, replacing matching
// pixels with the transparent placeholder, but the metadata does not yet carry
// a transparent index. Reserve one so those pixels encode as transparent.
metadata.HasTransparency = true;
if (palette.Length < 256)
@ -480,7 +485,7 @@ internal sealed class GifEncoderCore
metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
if (hasDuplicates)
if (requiresTransparency)
{
metadata.HasTransparency = true;
}
@ -492,11 +497,19 @@ internal sealed class GifEncoderCore
// Individual frames, though using the shared palette, can use a different transparent index
// to represent transparency.
// A difference was captured but the metadata does not have transparency.
if (hasDuplicates && !metadata.HasTransparency)
// The frame was de-duplicated against the previous frame, replacing matching pixels with
// the transparent placeholder. When the whole frame matches there is no captured difference,
// yet every pixel is still a placeholder, so we must always reserve a transparent index here;
// otherwise the placeholder pixels are matched to the nearest (typically darkest) palette color.
if (requiresTransparency && !metadata.HasTransparency)
{
metadata.HasTransparency = true;
transparencyIndex = globalFrameQuantizer.Palette.Length;
// Normally we pad one index past the palette so the (out of range) value is treated as
// transparent by decoders without growing the color table. A full 256-color palette leaves
// no room to pad within the 8-bit index space (index 256 wraps to 0 when written and exceeds
// the maximum GIF bit depth), so reuse the last in-range index for transparency instead.
transparencyIndex = Math.Min(globalFrameQuantizer.Palette.Length, byte.MaxValue);
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}

9
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -202,6 +202,15 @@ public readonly partial struct ErrorDither : IDither, IEquatable<ErrorDither>, I
ref TPixel pixel = ref rowSpan[targetX];
Vector4 result = pixel.ToVector4();
// Do not diffuse error into fully transparent pixels. They carry no visible color
// (a decoder shows whatever is behind them), so perturbing them is meaningless and,
// for indexed transparency, nudges them off the exact transparent color so they are
// matched to the nearest opaque palette entry instead of being kept transparent.
if (result.W <= 0)
{
continue;
}
result += error * coefficient;
pixel = TPixel.FromVector4(result);
}

10
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -182,6 +182,16 @@ public readonly partial struct OrderedDither : IDither, IEquatable<OrderedDither
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = source.ToRgba32();
// Leave fully transparent pixels untouched. They carry no visible color (a decoder shows
// whatever is behind them), so perturbing them is meaningless and, for indexed transparency,
// nudges them off the exact transparent color so they are matched to the nearest opaque
// palette entry instead of being kept transparent.
if (rgba.A == 0)
{
return source;
}
Unsafe.SkipInit(out Rgba32 attempt);
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale;

20
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -349,7 +350,7 @@ public class GifEncoderTests
public static string[] Animated => TestImages.Gif.Animated;
[Theory(Skip = "Enable for visual animated testing")]
[Theory]//(Skip = "Enable for visual animated testing")]
[WithFileCollection(nameof(Animated), PixelTypes.Rgba32)]
public void Encode_Animated_VisualTest<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
@ -436,4 +437,21 @@ public class GifEncoderTests
static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them.
image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate);
}
[Theory]
[WithFile(TestImages.Gif.Issues.Issue3142, PixelTypes.Rgba32)]
public void GifEncoder_CanDecode_AndEncode_Issue3142<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
// Save the image for visual inspection.
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { Quantizer = KnownQuantizers.Wu }, "animated");
// Now compare the debug output with the reference output.
// We do this because the gif encoding is lossy and encoding will lead to differences in the 10s of percent.
// From the unencoded image, we can see that the image is visually the same.
static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them.
image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate);
}
}

4
tests/ImageSharp.Tests/TestImages.cs

@ -614,6 +614,7 @@ public static class TestImages
public const string Issue2859_B = "Gif/issues/issue_2859_B.gif";
public const string Issue2953 = "Gif/issues/issue_2953.gif";
public const string Issue2980 = "Gif/issues/issue_2980.gif";
public const string Issue3142 = "Gif/issues/issue_3142.gif";
}
public static readonly string[] Animated =
@ -635,7 +636,8 @@ public static class TestImages
Issues.BadDescriptorWidth,
Issues.Issue1530,
Bit18RGBCube,
Global256NoTrans
Global256NoTrans,
Issues.Issue3142
];
}

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba81598dc214845f18064f987a55eebdab0e53149df8fd67904733b42c760ad6
size 7389

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba81598dc214845f18064f987a55eebdab0e53149df8fd67904733b42c760ad6
size 7389

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6bd47e66b07bab7edf53a07631016f8334ba337243d6b8d61b0c8a3cf7f3f0d
size 7974

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6bd47e66b07bab7edf53a07631016f8334ba337243d6b8d61b0c8a3cf7f3f0d
size 7974

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03d4e5cdaed2def942ca8480c9e45bb3d7914c6dbe3eac2b287afc8ff815b95d
size 8705

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ad084d64942ea1dec0c1b22a88400b47f14145d761aa22a8e7deb42b5bd9fb0
size 13011

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:714cf4be4fc6c4164494ca1950796211577ab61cbd939f7cc53a36c3f7f742c5
size 16457

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb04c45837b763abf2c326b3e831c5b772853d50c0bfc85fb7ac7ca62361292d
size 16872

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb04c45837b763abf2c326b3e831c5b772853d50c0bfc85fb7ac7ca62361292d
size 16872

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c68bad5aebb65e86e4295956b9f0bc21e181a6d8dadd9dbfbd5febf0a5ba0d8
size 17414

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d0672248b0e45ea700aa0c484ed0e342284ce18cad1cdc233700614636bed11
size 16556

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b12d34adf8c531c0d4ab02f7b57507a585bb1d31fb82d828c8c52732cef9f18
size 15590

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b12d34adf8c531c0d4ab02f7b57507a585bb1d31fb82d828c8c52732cef9f18
size 15590

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a31b9aee61fbc7d5dd5f163f2c81cb36140a05d788a7ed2d15de550c0fe13232
size 14561

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bc49ba55ee33a3aff5069b7a6bf45cdb429ad691f8701093ad7fe04abe25ea1
size 12156

4
tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:770061fbb29cd20bc700ce3fc57e38a758c632c3e89de51f5fbee3d5d522539e
size 912635
oid sha256:b33a960891c6b1e9cc6ac2ddc3cf49d8f49e0c749dfa7a67db49354988b129f6
size 935007

4
tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df34f8f3640b145add4f24f8003c288fe7991373b079a87b4be90842e18c82ae
size 8236
oid sha256:ad1630f3da7c2b997d04c75de2ac1465255c977454461f2fdc7026b3f87e11f1
size 8232

3
tests/Images/Input/Gif/issues/issue_3142.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:982c394dbee297bb3bc5cc532827742260acb6e5aa07e1dca3f0b17e57bc3bbe
size 386565
Loading…
Cancel
Save