Skip to content

Commit 4a8ff2b

Browse files
Merge pull request #2882 from SixLabors/js/fix-2866
V3 : Fix GIF, PNG, and WEBP Edge Case Handling
2 parents 520e597 + 925a651 commit 4a8ff2b

File tree

190 files changed

+2005
-879
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

190 files changed

+2005
-879
lines changed

src/ImageSharp/Advanced/AotCompilerTools.cs

+15
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ private static void Seed<TPixel>()
138138
AotCompileResamplers<TPixel>();
139139
AotCompileQuantizers<TPixel>();
140140
AotCompilePixelSamplingStrategys<TPixel>();
141+
AotCompilePixelMaps<TPixel>();
141142
AotCompileDithers<TPixel>();
142143
AotCompileMemoryManagers<TPixel>();
143144

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

518+
/// <summary>
519+
/// This method pre-seeds the all <see cref="IColorIndexCache{T}" /> in the AoT compiler.
520+
/// </summary>
521+
/// <typeparam name="TPixel">The pixel format.</typeparam>
522+
[Preserve]
523+
private static void AotCompilePixelMaps<TPixel>()
524+
where TPixel : unmanaged, IPixel<TPixel>
525+
{
526+
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
527+
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
528+
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
529+
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
530+
}
531+
517532
/// <summary>
518533
/// This method pre-seeds the all <see cref="IDither" /> in the AoT compiler.
519534
/// </summary>

src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.Formats;
5+
56
internal class AnimatedImageFrameMetadata
67
{
78
/// <summary>

src/ImageSharp/Formats/AnimatedImageMetadata.cs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.Formats;
5+
56
internal class AnimatedImageMetadata
67
{
78
/// <summary>

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

+127-42
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
8989
/// </summary>
9090
private GifMetadata? gifMetadata;
9191

92+
/// <summary>
93+
/// The background color index.
94+
/// </summary>
95+
private byte backgroundColorIndex;
96+
9297
/// <summary>
9398
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
9499
/// </summary>
@@ -108,6 +113,10 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
108113
uint frameCount = 0;
109114
Image<TPixel>? image = null;
110115
ImageFrame<TPixel>? previousFrame = null;
116+
GifDisposalMethod? previousDisposalMethod = null;
117+
bool globalColorTableUsed = false;
118+
Color backgroundColor = Color.Transparent;
119+
111120
try
112121
{
113122
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@@ -123,7 +132,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
123132
break;
124133
}
125134

126-
this.ReadFrame(stream, ref image, ref previousFrame);
135+
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, ref backgroundColor);
127136

128137
// Reset per-frame state.
129138
this.imageDescriptor = default;
@@ -158,6 +167,13 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
158167
break;
159168
}
160169
}
170+
171+
// We cannot always trust the global GIF palette has actually been used.
172+
// https://github.com/SixLabors/ImageSharp/issues/2866
173+
if (!globalColorTableUsed)
174+
{
175+
this.gifMetadata.ColorTableMode = GifColorTableMode.Local;
176+
}
161177
}
162178
finally
163179
{
@@ -417,7 +433,14 @@ private void ReadComments(BufferedReadStream stream)
417433
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
418434
/// <param name="image">The image to decode the information to.</param>
419435
/// <param name="previousFrame">The previous frame.</param>
420-
private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
436+
/// <param name="previousDisposalMethod">The previous disposal method.</param>
437+
/// <param name="backgroundColor">The background color.</param>
438+
private bool ReadFrame<TPixel>(
439+
BufferedReadStream stream,
440+
ref Image<TPixel>? image,
441+
ref ImageFrame<TPixel>? previousFrame,
442+
ref GifDisposalMethod? previousDisposalMethod,
443+
ref Color backgroundColor)
421444
where TPixel : unmanaged, IPixel<TPixel>
422445
{
423446
this.ReadImageDescriptor(stream);
@@ -444,10 +467,52 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
444467
}
445468

446469
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
447-
this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
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+
}
448511

449512
// Skip any remaining blocks
450513
SkipBlock(stream);
514+
515+
return !hasLocalColorTable;
451516
}
452517

453518
/// <summary>
@@ -457,57 +522,74 @@ private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
457522
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
458523
/// <param name="image">The image to decode the information to.</param>
459524
/// <param name="previousFrame">The previous frame.</param>
525+
/// <param name="previousDisposalMethod">The previous disposal method.</param>
460526
/// <param name="colorTable">The color table containing the available colors.</param>
461527
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
528+
/// <param name="backgroundPixel">The background color pixel.</param>
462529
private void ReadFrameColors<TPixel>(
463530
BufferedReadStream stream,
464531
ref Image<TPixel>? image,
465532
ref ImageFrame<TPixel>? previousFrame,
533+
ref GifDisposalMethod? previousDisposalMethod,
466534
ReadOnlySpan<Rgb24> colorTable,
467-
in GifImageDescriptor descriptor)
535+
in GifImageDescriptor descriptor,
536+
TPixel backgroundPixel)
468537
where TPixel : unmanaged, IPixel<TPixel>
469538
{
470539
int imageWidth = this.logicalScreenDescriptor.Width;
471540
int imageHeight = this.logicalScreenDescriptor.Height;
472541
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
542+
GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod;
543+
ImageFrame<TPixel> currentFrame;
544+
ImageFrame<TPixel>? restoreFrame = null;
473545

474-
ImageFrame<TPixel>? prevFrame = null;
475-
ImageFrame<TPixel>? currentFrame = null;
476-
ImageFrame<TPixel> imageFrame;
546+
if (previousFrame is null && previousDisposalMethod is null)
547+
{
548+
image = transFlag
549+
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
550+
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);
477551

478-
if (previousFrame is null)
552+
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
553+
currentFrame = image.Frames.RootFrame;
554+
}
555+
else
479556
{
480-
if (!transFlag)
557+
if (previousFrame != null)
481558
{
482-
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
559+
currentFrame = image!.Frames.AddFrame(previousFrame);
483560
}
484561
else
485562
{
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);
563+
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
488564
}
489565

490-
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
566+
this.SetFrameMetadata(currentFrame.Metadata);
491567

492-
imageFrame = image.Frames.RootFrame;
493-
}
494-
else
495-
{
496568
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
497569
{
498-
prevFrame = previousFrame;
570+
restoreFrame = previousFrame;
499571
}
500572

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

506-
this.SetFrameMetadata(currentFrame.Metadata);
579+
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
580+
{
581+
previousFrame = restoreFrame;
582+
}
583+
else
584+
{
585+
previousFrame = currentFrame;
586+
}
507587

508-
imageFrame = currentFrame;
588+
previousDisposalMethod = disposalMethod;
509589

510-
this.RestoreToBackground(imageFrame);
590+
if (disposalMethod == GifDisposalMethod.RestoreToBackground)
591+
{
592+
this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
511593
}
512594

513595
if (colorTable.Length == 0)
@@ -573,7 +655,7 @@ private void ReadFrameColors<TPixel>(
573655
}
574656

575657
lzwDecoder.DecodePixelRow(indicesRow);
576-
ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
658+
ref TPixel rowRef = ref MemoryMarshal.GetReference(currentFrame.PixelBuffer.DangerousGetRowSpan(writeY));
577659

578660
if (!transFlag)
579661
{
@@ -605,19 +687,6 @@ private void ReadFrameColors<TPixel>(
605687
}
606688
}
607689
}
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-
}
621690
}
622691

623692
/// <summary>
@@ -638,6 +707,11 @@ private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadat
638707
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
639708
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
640709
}
710+
else
711+
{
712+
this.currentLocalColorTable = null;
713+
this.currentLocalColorTableSize = 0;
714+
}
641715

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

673749
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
674750
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
675-
pixelRegion.Clear();
751+
if (transparent)
752+
{
753+
pixelRegion.Clear();
754+
}
755+
else
756+
{
757+
pixelRegion.Fill(background);
758+
}
676759

677760
this.restoreArea = null;
678761
}
@@ -787,7 +870,9 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
787870
}
788871
}
789872

790-
this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
873+
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
874+
this.backgroundColorIndex = index;
875+
this.gifMetadata.BackgroundColorIndex = index;
791876
}
792877

793878
private unsafe struct ScratchBuffer

0 commit comments

Comments
 (0)