Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/ImageSharp/Color/Color.WernerPalette.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ private static Color[] CreateWernerPalette() =>
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 changes: 22 additions & 9 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ private void EncodeAdditionalFrame<TPixel>(
: 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,
Expand All @@ -378,7 +381,7 @@ private void EncodeAdditionalFrame<TPixel>(
bounds,
metadata,
useLocal,
difference,
true,
transparencyIndex,
background);

Expand All @@ -403,7 +406,7 @@ private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
bool hasDuplicates,
bool requiresTransparency,
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
Expand All @@ -417,9 +420,11 @@ private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(
// 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)
Expand Down Expand Up @@ -480,7 +485,7 @@ private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(

metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);

if (hasDuplicates)
if (requiresTransparency)
{
metadata.HasTransparency = true;
}
Expand All @@ -492,11 +497,19 @@ private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>(
// 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);
}

Expand Down
9 changes: 9 additions & 0 deletions src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ internal TPixel Dither<TPixel>(
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);
}
Expand Down
10 changes: 10 additions & 0 deletions src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ internal TPixel Dither<TPixel>(
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;
Expand Down
20 changes: 19 additions & 1 deletion tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;

Expand Down Expand Up @@ -349,7 +350,7 @@ public void Encode_AnimatedFormatTransform_FromWebp<TPixel>(TestImageProvider<TP

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>
Expand Down Expand Up @@ -436,4 +437,21 @@ public void GifEncoder_CanDecode_AndEncode_Issue2866<TPixel>(TestImageProvider<T
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 changes: 3 additions & 1 deletion tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ public static class Issues
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 =
Expand All @@ -635,7 +636,8 @@ public static class Issues
Issues.BadDescriptorWidth,
Issues.Issue1530,
Bit18RGBCube,
Global256NoTrans
Global256NoTrans,
Issues.Issue3142
];
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Gif/issues/issue_3142.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading