Skip to content

Commit 2b239ec

Browse files
Fix GIF restore to background behavior and background color assignment.
1 parent bd1649d commit 2b239ec

File tree

3 files changed

+72
-46
lines changed

3 files changed

+72
-46
lines changed

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ internal sealed class GifDecoderCore : ImageDecoderCore
8989
/// </summary>
9090
private GifMetadata? gifMetadata;
9191

92+
// The background color used to fill the frame.
93+
private Color backgroundColor;
94+
9295
/// <summary>
9396
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
9497
/// </summary>
@@ -108,9 +111,12 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
108111
uint frameCount = 0;
109112
Image<TPixel>? image = null;
110113
ImageFrame<TPixel>? previousFrame = null;
114+
GifDisposalMethod? previousDisposalMethod = null;
115+
111116
try
112117
{
113118
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
119+
TPixel backgroundPixel = this.backgroundColor.ToPixel<TPixel>();
114120

115121
// Loop though the respective gif parts and read the data.
116122
int nextFlag = stream.ReadByte();
@@ -123,7 +129,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
123129
break;
124130
}
125131

126-
this.ReadFrame(stream, ref image, ref previousFrame);
132+
this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel);
127133

128134
// Reset per-frame state.
129135
this.imageDescriptor = default;
@@ -417,7 +423,14 @@ private void ReadComments(BufferedReadStream stream)
417423
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
418424
/// <param name="image">The image to decode the information to.</param>
419425
/// <param name="previousFrame">The previous frame.</param>
420-
private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
426+
/// <param name="previousDisposalMethod">The previous disposal method.</param>
427+
/// <param name="backgroundPixel">The background color pixel.</param>
428+
private void ReadFrame<TPixel>(
429+
BufferedReadStream stream,
430+
ref Image<TPixel>? image,
431+
ref ImageFrame<TPixel>? previousFrame,
432+
ref GifDisposalMethod? previousDisposalMethod,
433+
TPixel backgroundPixel)
421434
where TPixel : unmanaged, IPixel<TPixel>
422435
{
423436
this.ReadImageDescriptor(stream);
@@ -444,7 +457,7 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
444457
}
445458

446459
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
447-
this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
460+
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundPixel);
448461

449462
// Skip any remaining blocks
450463
SkipBlock(stream);
@@ -457,57 +470,58 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
457470
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
458471
/// <param name="image">The image to decode the information to.</param>
459472
/// <param name="previousFrame">The previous frame.</param>
473+
/// <param name="previousDisposalMethod">The previous disposal method.</param>
460474
/// <param name="colorTable">The color table containing the available colors.</param>
461475
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
476+
/// <param name="backgroundPixel">The background color pixel.</param>
462477
private void ReadFrameColors<TPixel>(
463478
BufferedReadStream stream,
464479
ref Image<TPixel>? image,
465480
ref ImageFrame<TPixel>? previousFrame,
481+
ref GifDisposalMethod? previousDisposalMethod,
466482
ReadOnlySpan<Rgb24> colorTable,
467-
in GifImageDescriptor descriptor)
483+
in GifImageDescriptor descriptor,
484+
TPixel backgroundPixel)
468485
where TPixel : unmanaged, IPixel<TPixel>
469486
{
470487
int imageWidth = this.logicalScreenDescriptor.Width;
471488
int imageHeight = this.logicalScreenDescriptor.Height;
472489
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
473490

474-
ImageFrame<TPixel>? prevFrame = null;
475-
ImageFrame<TPixel>? currentFrame = null;
476-
ImageFrame<TPixel> imageFrame;
491+
GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod;
477492

493+
ImageFrame<TPixel> currentFrame;
478494
if (previousFrame is null)
479495
{
480-
if (!transFlag)
481-
{
482-
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
483-
}
484-
else
485-
{
486-
// This initializes the image to become fully transparent because the alpha channel is zero.
487-
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
488-
}
496+
image = transFlag
497+
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
498+
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);
489499

490500
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
491-
492-
imageFrame = image.Frames.RootFrame;
501+
currentFrame = image.Frames.RootFrame;
493502
}
494503
else
495504
{
496-
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
497-
{
498-
prevFrame = previousFrame;
499-
}
500-
501505
// We create a clone of the frame and add it.
502506
// We will overpaint the difference of pixels on the current frame to create a complete image.
503507
// This ensures that we have enough pixel data to process without distortion. #2450
504508
currentFrame = image!.Frames.AddFrame(previousFrame);
505509

506510
this.SetFrameMetadata(currentFrame.Metadata);
507511

508-
imageFrame = currentFrame;
512+
if (previousDisposalMethod == GifDisposalMethod.RestoreToBackground)
513+
{
514+
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag);
515+
}
516+
}
517+
518+
Rectangle interest = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
519+
previousFrame = currentFrame;
520+
previousDisposalMethod = disposalMethod;
509521

510-
this.RestoreToBackground(imageFrame);
522+
if (disposalMethod == GifDisposalMethod.RestoreToBackground)
523+
{
524+
this.restoreArea = interest;
511525
}
512526

513527
if (colorTable.Length == 0)
@@ -573,7 +587,7 @@ private void ReadFrameColors<TPixel>(
573587
}
574588

575589
lzwDecoder.DecodePixelRow(indicesRow);
576-
ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
590+
ref TPixel rowRef = ref MemoryMarshal.GetReference(currentFrame.PixelBuffer.DangerousGetRowSpan(writeY));
577591

578592
if (!transFlag)
579593
{
@@ -605,19 +619,6 @@ private void ReadFrameColors<TPixel>(
605619
}
606620
}
607621
}
608-
609-
if (prevFrame != null)
610-
{
611-
previousFrame = prevFrame;
612-
return;
613-
}
614-
615-
previousFrame = currentFrame ?? image.Frames.RootFrame;
616-
617-
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground)
618-
{
619-
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
620-
}
621622
}
622623

623624
/// <summary>
@@ -638,6 +639,11 @@ private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadat
638639
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
639640
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
640641
}
642+
else
643+
{
644+
this.currentLocalColorTable = null;
645+
this.currentLocalColorTableSize = 0;
646+
}
641647

642648
// Skip the frame indices. Pixels length + mincode size.
643649
// The gif format does not tell us the length of the compressed data beforehand.
@@ -662,7 +668,9 @@ private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadat
662668
/// </summary>
663669
/// <typeparam name="TPixel">The pixel format.</typeparam>
664670
/// <param name="frame">The frame.</param>
665-
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
671+
/// <param name="background">The background color.</param>
672+
/// <param name="transparent">Whether the background is transparent.</param>
673+
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame, TPixel background, bool transparent)
666674
where TPixel : unmanaged, IPixel<TPixel>
667675
{
668676
if (this.restoreArea is null)
@@ -672,7 +680,14 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
672680

673681
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
674682
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
675-
pixelRegion.Clear();
683+
if (transparent)
684+
{
685+
pixelRegion.Clear();
686+
}
687+
else
688+
{
689+
pixelRegion.Fill(background);
690+
}
676691

677692
this.restoreArea = null;
678693
}
@@ -787,7 +802,18 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
787802
}
788803
}
789804

790-
this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
805+
// If the global color table is present, we can set the background color
806+
// otherwise we default to transparent to match browser behavior.
807+
ReadOnlyMemory<Color>? table = this.gifMetadata.GlobalColorTable;
808+
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
809+
if (table is not null && index < table.Value.Length)
810+
{
811+
this.backgroundColor = table.Value.Span[index];
812+
}
813+
else
814+
{
815+
this.backgroundColor = Color.Transparent;
816+
}
791817
}
792818

793819
private unsafe struct ScratchBuffer
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)