Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 : Fix GIF, PNG, and WEBP Edge Case Handling #2882

Merged
merged 29 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6f38753
Add image and test
JimBobSquarePants Feb 4, 2025
a888544
Fix GIF handling of unused global tables.
JimBobSquarePants Feb 6, 2025
ef8c79d
Fix WEBP animation disposal and blending
JimBobSquarePants Feb 6, 2025
bd1649d
Ensure WEBP decoder unsets the restore area.
JimBobSquarePants Feb 7, 2025
2b239ec
Fix GIF restore to background behavior and background color assignment.
JimBobSquarePants Feb 7, 2025
f63ad84
Remove TODO:
JimBobSquarePants Feb 7, 2025
7cecea9
Revert breaking change
JimBobSquarePants Feb 7, 2025
9c5bcfa
Update build-and-test.yml
JimBobSquarePants Feb 7, 2025
386e17d
Merge branch 'release/3.1.x' into js/fix-2866
JimBobSquarePants Feb 10, 2025
5d77de9
Fix PNG animation encoding and quantizer output
JimBobSquarePants Feb 24, 2025
6da9bc3
Fix transparency mode, update quantized refs
JimBobSquarePants Feb 25, 2025
33e5cbf
Try bumping to latest SDK
JimBobSquarePants Feb 25, 2025
c4d314a
Revert "Try bumping to latest SDK"
JimBobSquarePants Feb 25, 2025
67fd9de
Try casting
JimBobSquarePants Feb 25, 2025
d33e6a9
Use latest instead of preview to avoid build errors.
JimBobSquarePants Feb 26, 2025
f9f5257
Try explicit langversion
JimBobSquarePants Feb 26, 2025
f63e1a4
Try langversion 12
JimBobSquarePants Feb 26, 2025
94df8e3
Update src/ImageSharp/Processing/Processors/Quantization/EuclideanPix…
JimBobSquarePants Mar 3, 2025
f80aa76
Additional testcases for gif decoder
brianpopow Mar 3, 2025
78b902b
Merge branch 'release/3.1.x' into js/fix-2866
JimBobSquarePants Mar 5, 2025
6430b8e
Fix GIF RestoreToPrevious
JimBobSquarePants Mar 6, 2025
bf66f24
Respond to feedback
JimBobSquarePants Mar 11, 2025
b65abe7
Add options for color lookup.
JimBobSquarePants Mar 12, 2025
eb6b0ab
Do not copy color tables when transcoding.
JimBobSquarePants Mar 12, 2025
71357d2
Fix PNG encoder blending.
JimBobSquarePants Mar 24, 2025
4ace814
extend gif benchmarks
antonfirsov Mar 25, 2025
0ea2404
Revert to coarse cache default, additional feedback.
JimBobSquarePants Mar 26, 2025
efe11dc
Fix PNG chunk detection.
JimBobSquarePants Mar 26, 2025
925a651
fix build (add blank lines)
antonfirsov Mar 27, 2025
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
15 changes: 15 additions & 0 deletions src/ImageSharp/Advanced/AotCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ private static void Seed<TPixel>()
AotCompileResamplers<TPixel>();
AotCompileQuantizers<TPixel>();
AotCompilePixelSamplingStrategys<TPixel>();
AotCompilePixelMaps<TPixel>();
AotCompileDithers<TPixel>();
AotCompileMemoryManagers<TPixel>();

Expand Down Expand Up @@ -514,6 +515,20 @@ private static void AotCompilePixelSamplingStrategys<TPixel>()
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
}

/// <summary>
/// This method pre-seeds the all <see cref="IColorIndexCache{T}" /> in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}

/// <summary>
/// This method pre-seeds the all <see cref="IDither" /> in the AoT compiler.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats;

internal class AnimatedImageFrameMetadata
{
/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp/Formats/AnimatedImageMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats;

internal class AnimatedImageMetadata
{
/// <summary>
Expand Down
169 changes: 127 additions & 42 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// </summary>
private GifMetadata? gifMetadata;

/// <summary>
/// The background color index.
/// </summary>
private byte backgroundColorIndex;

/// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary>
Expand All @@ -108,6 +113,10 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
uint frameCount = 0;
Image<TPixel>? image = null;
ImageFrame<TPixel>? previousFrame = null;
GifDisposalMethod? previousDisposalMethod = null;
bool globalColorTableUsed = false;
Color backgroundColor = Color.Transparent;

try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
Expand All @@ -123,7 +132,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
break;
}

this.ReadFrame(stream, ref image, ref previousFrame);
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, ref backgroundColor);

// Reset per-frame state.
this.imageDescriptor = default;
Expand Down Expand Up @@ -158,6 +167,13 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
break;
}
}

// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = GifColorTableMode.Local;
}
}
finally
{
Expand Down Expand Up @@ -417,7 +433,14 @@ private void ReadComments(BufferedReadStream stream)
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
/// <param name="previousDisposalMethod">The previous disposal method.</param>
/// <param name="backgroundColor">The background color.</param>
private bool ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref GifDisposalMethod? previousDisposalMethod,
ref Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ReadImageDescriptor(stream);
Expand All @@ -444,10 +467,52 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
}

ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);

// First frame
if (image is null)
{
if (this.backgroundColorIndex < colorTable.Length)
{
backgroundColor = colorTable[this.backgroundColorIndex];
}
else
{
backgroundColor = Color.Transparent;
}

if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
}

this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundColor.ToPixel<TPixel>());

// Update from newly decoded frame.
if (this.graphicsControlExtension.DisposalMethod != GifDisposalMethod.RestoreToPrevious)
{
if (this.backgroundColorIndex < colorTable.Length)
{
backgroundColor = colorTable[this.backgroundColorIndex];
}
else
{
backgroundColor = Color.Transparent;
}

// TODO: I don't understand why this is always set to alpha of zero.
// This should be dependent on the transparency flag of the graphics
// control extension. ImageMagick does the same.
// if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
}

// Skip any remaining blocks
SkipBlock(stream);

return !hasLocalColorTable;
}

/// <summary>
Expand All @@ -457,57 +522,74 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="previousDisposalMethod">The previous disposal method.</param>
/// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
/// <param name="backgroundPixel">The background color pixel.</param>
private void ReadFrameColors<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref GifDisposalMethod? previousDisposalMethod,
ReadOnlySpan<Rgb24> colorTable,
in GifImageDescriptor descriptor)
in GifImageDescriptor descriptor,
TPixel backgroundPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame;
ImageFrame<TPixel>? restoreFrame = null;

ImageFrame<TPixel>? prevFrame = null;
ImageFrame<TPixel>? currentFrame = null;
ImageFrame<TPixel> imageFrame;
if (previousFrame is null && previousDisposalMethod is null)
{
image = transFlag
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);

if (previousFrame is null)
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
currentFrame = image.Frames.RootFrame;
}
else
{
if (!transFlag)
if (previousFrame != null)
{
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
currentFrame = image!.Frames.AddFrame(previousFrame);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
}

this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
this.SetFrameMetadata(currentFrame.Metadata);

imageFrame = image.Frames.RootFrame;
}
else
{
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
{
prevFrame = previousFrame;
restoreFrame = previousFrame;
}

// We create a clone of the frame and add it.
// We will overpaint the difference of pixels on the current frame to create a complete image.
// This ensures that we have enough pixel data to process without distortion. #2450
currentFrame = image!.Frames.AddFrame(previousFrame);
if (previousDisposalMethod == GifDisposalMethod.RestoreToBackground)
{
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag);
}
}

this.SetFrameMetadata(currentFrame.Metadata);
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
{
previousFrame = restoreFrame;
}
else
{
previousFrame = currentFrame;
}

imageFrame = currentFrame;
previousDisposalMethod = disposalMethod;

this.RestoreToBackground(imageFrame);
if (disposalMethod == GifDisposalMethod.RestoreToBackground)
{
this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
}

if (colorTable.Length == 0)
Expand Down Expand Up @@ -573,7 +655,7 @@ private void ReadFrameColors<TPixel>(
}

lzwDecoder.DecodePixelRow(indicesRow);
ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
ref TPixel rowRef = ref MemoryMarshal.GetReference(currentFrame.PixelBuffer.DangerousGetRowSpan(writeY));

if (!transFlag)
{
Expand Down Expand Up @@ -605,19 +687,6 @@ private void ReadFrameColors<TPixel>(
}
}
}

if (prevFrame != null)
{
previousFrame = prevFrame;
return;
}

previousFrame = currentFrame ?? image.Frames.RootFrame;

if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground)
{
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
}
}

/// <summary>
Expand All @@ -638,6 +707,11 @@ private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadat
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
else
{
this.currentLocalColorTable = null;
this.currentLocalColorTableSize = 0;
}

// Skip the frame indices. Pixels length + mincode size.
// The gif format does not tell us the length of the compressed data beforehand.
Expand All @@ -662,7 +736,9 @@ private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadat
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The frame.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
/// <param name="background">The background color.</param>
/// <param name="transparent">Whether the background is transparent.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame, TPixel background, bool transparent)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.restoreArea is null)
Expand All @@ -672,7 +748,14 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)

Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
if (transparent)
{
pixelRegion.Clear();
}
else
{
pixelRegion.Fill(background);
}

this.restoreArea = null;
}
Expand Down Expand Up @@ -787,7 +870,9 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
}
}

this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
this.backgroundColorIndex = index;
this.gifMetadata.BackgroundColorIndex = index;
}

private unsafe struct ScratchBuffer
Expand Down
Loading
Loading