Skip to content

Commit 6430b8e

Browse files
Fix GIF RestoreToPrevious
1 parent 78b902b commit 6430b8e

File tree

3 files changed

+81
-33
lines changed

3 files changed

+81
-33
lines changed

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ 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;
92+
/// <summary>
93+
/// The background color index.
94+
/// </summary>
95+
private byte backgroundColorIndex;
9496

9597
/// <summary>
9698
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
@@ -113,11 +115,11 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
113115
ImageFrame<TPixel>? previousFrame = null;
114116
GifDisposalMethod? previousDisposalMethod = null;
115117
bool globalColorTableUsed = false;
118+
Color backgroundColor = Color.Transparent;
116119

117120
try
118121
{
119122
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
120-
TPixel backgroundPixel = this.backgroundColor.ToPixel<TPixel>();
121123

122124
// Loop though the respective gif parts and read the data.
123125
int nextFlag = stream.ReadByte();
@@ -130,7 +132,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
130132
break;
131133
}
132134

133-
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel);
135+
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, ref backgroundColor);
134136

135137
// Reset per-frame state.
136138
this.imageDescriptor = default;
@@ -432,13 +434,13 @@ private void ReadComments(BufferedReadStream stream)
432434
/// <param name="image">The image to decode the information to.</param>
433435
/// <param name="previousFrame">The previous frame.</param>
434436
/// <param name="previousDisposalMethod">The previous disposal method.</param>
435-
/// <param name="backgroundPixel">The background color pixel.</param>
437+
/// <param name="backgroundColor">The background color.</param>
436438
private bool ReadFrame<TPixel>(
437439
BufferedReadStream stream,
438440
ref Image<TPixel>? image,
439441
ref ImageFrame<TPixel>? previousFrame,
440442
ref GifDisposalMethod? previousDisposalMethod,
441-
TPixel backgroundPixel)
443+
ref Color backgroundColor)
442444
where TPixel : unmanaged, IPixel<TPixel>
443445
{
444446
this.ReadImageDescriptor(stream);
@@ -465,7 +467,47 @@ private bool ReadFrame<TPixel>(
465467
}
466468

467469
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
468-
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundPixel);
470+
471+
// First frame
472+
if (image is null)
473+
{
474+
if (this.backgroundColorIndex < colorTable.Length)
475+
{
476+
backgroundColor = colorTable[this.backgroundColorIndex];
477+
}
478+
else
479+
{
480+
backgroundColor = Color.Transparent;
481+
}
482+
483+
if (this.graphicsControlExtension.TransparencyFlag)
484+
{
485+
backgroundColor = backgroundColor.WithAlpha(0);
486+
}
487+
}
488+
489+
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundColor.ToPixel<TPixel>());
490+
491+
// Update from newly decoded frame.
492+
if (this.graphicsControlExtension.DisposalMethod != GifDisposalMethod.RestoreToPrevious)
493+
{
494+
if (this.backgroundColorIndex < colorTable.Length)
495+
{
496+
backgroundColor = colorTable[this.backgroundColorIndex];
497+
}
498+
else
499+
{
500+
backgroundColor = Color.Transparent;
501+
}
502+
503+
// TODO: I don't understand why this is always set to alpha of zero.
504+
// This should be dependent on the transparency flag of the graphics
505+
// control extension. ImageMagick does the same.
506+
// if (this.graphicsControlExtension.TransparencyFlag)
507+
{
508+
backgroundColor = backgroundColor.WithAlpha(0);
509+
}
510+
}
469511

470512
// Skip any remaining blocks
471513
SkipBlock(stream);
@@ -497,11 +539,11 @@ private void ReadFrameColors<TPixel>(
497539
int imageWidth = this.logicalScreenDescriptor.Width;
498540
int imageHeight = this.logicalScreenDescriptor.Height;
499541
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
500-
501542
GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod;
502-
503543
ImageFrame<TPixel> currentFrame;
504-
if (previousFrame is null)
544+
ImageFrame<TPixel>? restoreFrame = null;
545+
546+
if (previousFrame is null && previousDisposalMethod is null)
505547
{
506548
image = transFlag
507549
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
@@ -512,26 +554,42 @@ private void ReadFrameColors<TPixel>(
512554
}
513555
else
514556
{
515-
// We create a clone of the frame and add it.
516-
// We will overpaint the difference of pixels on the current frame to create a complete image.
517-
// This ensures that we have enough pixel data to process without distortion. #2450
518-
currentFrame = image!.Frames.AddFrame(previousFrame);
557+
if (previousFrame != null)
558+
{
559+
currentFrame = image!.Frames.AddFrame(previousFrame);
560+
}
561+
else
562+
{
563+
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
564+
}
519565

520566
this.SetFrameMetadata(currentFrame.Metadata);
521567

568+
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
569+
{
570+
restoreFrame = previousFrame;
571+
}
572+
522573
if (previousDisposalMethod == GifDisposalMethod.RestoreToBackground)
523574
{
524575
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag);
525576
}
526577
}
527578

528-
Rectangle interest = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
529-
previousFrame = currentFrame;
579+
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
580+
{
581+
previousFrame = restoreFrame;
582+
}
583+
else
584+
{
585+
previousFrame = currentFrame;
586+
}
587+
530588
previousDisposalMethod = disposalMethod;
531589

532590
if (disposalMethod == GifDisposalMethod.RestoreToBackground)
533591
{
534-
this.restoreArea = interest;
592+
this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
535593
}
536594

537595
if (colorTable.Length == 0)
@@ -812,19 +870,9 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
812870
}
813871
}
814872

815-
// If the global color table is present, we can set the background color
816-
// otherwise we default to transparent to match browser behavior.
817-
ReadOnlyMemory<Color>? table = this.gifMetadata.GlobalColorTable;
818873
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
819-
if (table is not null && index < table.Value.Length)
820-
{
821-
this.backgroundColor = table.Value.Span[index];
822-
this.gifMetadata.BackgroundColorIndex = index;
823-
}
824-
else
825-
{
826-
this.backgroundColor = Color.Transparent;
827-
}
874+
this.backgroundColorIndex = index;
875+
this.gifMetadata.BackgroundColorIndex = index;
828876
}
829877

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

0 commit comments

Comments
 (0)