diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs
index 6d6cfc0792..d0682e5413 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs
@@ -23,6 +23,11 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe
///
public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All;
+ ///
+ /// Gets or sets the maximum number of gif frames.
+ ///
+ public uint MaxFrames { get; set; } = uint.MaxValue;
+
///
public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index d17e89cd45..8fdaef9e0f 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
+using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -22,19 +23,24 @@ namespace SixLabors.ImageSharp.Formats.Gif
internal sealed class GifDecoderCore : IImageDecoderInternals
{
///
- /// The temp buffer used to reduce allocations.
+ /// The temp buffer.
///
- private readonly byte[] buffer = new byte[16];
+ private byte[] buffer = new byte[16];
///
- /// The currently loaded stream.
+ /// The global color table.
///
- private BufferedReadStream stream;
+ private IMemoryOwner globalColorTable;
///
- /// The global color table.
+ /// The current local color table.
///
- private IMemoryOwner globalColorTable;
+ private IMemoryOwner currentLocalColorTable;
+
+ ///
+ /// Gets the size in bytes of the current local color table.
+ ///
+ private int currentLocalColorTableSize;
///
/// The area to restore.
@@ -56,6 +62,26 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
///
private GifImageDescriptor imageDescriptor;
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The maximum number of frames to decode. Inclusive.
+ ///
+ private readonly uint maxFrames;
+
+ ///
+ /// Whether to skip metadata during decode.
+ ///
+ private readonly bool skipMetadata;
+
///
/// The abstract metadata.
///
@@ -73,23 +99,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// The decoder options.
public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
{
- this.IgnoreMetadata = options.IgnoreMetadata;
- this.DecodingMode = options.DecodingMode;
- this.Configuration = configuration ?? Configuration.Default;
+ this.skipMetadata = options.IgnoreMetadata;
+ this.configuration = configuration ?? Configuration.Default;
+ this.maxFrames = options.DecodingMode == FrameDecodingMode.All ? options.MaxFrames : 1;
+ this.memoryAllocator = this.configuration.MemoryAllocator;
}
///
- public Configuration Configuration { get; }
-
- ///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
- ///
- public bool IgnoreMetadata { get; internal set; }
-
- ///
- /// Gets the decoding mode for multi-frame images.
- ///
- public FrameDecodingMode DecodingMode { get; }
+ public Configuration Configuration => this.configuration;
///
/// Gets the dimensions of the image.
@@ -102,6 +119,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ uint frameCount = 0;
Image image = null;
ImageFrame previousFrame = null;
try
@@ -114,28 +132,32 @@ public Image Decode(BufferedReadStream stream, CancellationToken
{
if (nextFlag == GifConstants.ImageLabel)
{
- if (previousFrame != null && this.DecodingMode == FrameDecodingMode.First)
+ if (previousFrame != null && ++frameCount == this.maxFrames)
{
break;
}
- this.ReadFrame(ref image, ref previousFrame);
+ this.ReadFrame(stream, ref image, ref previousFrame);
+
+ // Reset per-frame state.
+ this.imageDescriptor = default;
+ this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
- this.ReadGraphicalControlExtension();
+ this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
- this.ReadComments();
+ this.ReadComments(stream);
break;
case GifConstants.ApplicationExtensionLabel:
- this.ReadApplicationExtension();
+ this.ReadApplicationExtension(stream);
break;
case GifConstants.PlainTextLabel:
- this.SkipBlock(); // Not supported by any known decoder.
+ SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
@@ -154,6 +176,12 @@ public Image Decode(BufferedReadStream stream, CancellationToken
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
+ }
+
+ if (image is null)
+ {
+ GifThrowHelper.ThrowInvalidImageContentException("No data");
}
return image;
@@ -162,6 +190,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken
///
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
+ uint frameCount = 0;
+ ImageFrameMetadata? previousFrame = null;
+ List framesMetadata = new();
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@@ -172,23 +203,32 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
{
if (nextFlag == GifConstants.ImageLabel)
{
- this.ReadImageDescriptor();
+ if (previousFrame != null && ++frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
+
+ // Reset per-frame state.
+ this.imageDescriptor = default;
+ this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
- this.SkipBlock(); // Skip graphic control extension block
+ this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
- this.ReadComments();
+ this.ReadComments(stream);
break;
case GifConstants.ApplicationExtensionLabel:
- this.ReadApplicationExtension();
+ this.ReadApplicationExtension(stream);
break;
case GifConstants.PlainTextLabel:
- this.SkipBlock(); // Not supported by any known decoder.
+ SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
@@ -207,6 +247,12 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
+ }
+
+ if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
+ {
+ GifThrowHelper.ThrowNoHeader();
}
return new ImageInfo(
@@ -219,9 +265,10 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
///
/// Reads the graphic control extension.
///
- private void ReadGraphicalControlExtension()
+ /// The containing image data.
+ private void ReadGraphicalControlExtension(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 6);
+ int bytesRead = stream.Read(this.buffer, 0, 6);
if (bytesRead != 6)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
@@ -233,9 +280,10 @@ private void ReadGraphicalControlExtension()
///
/// Reads the image descriptor.
///
- private void ReadImageDescriptor()
+ /// The containing image data.
+ private void ReadImageDescriptor(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 9);
+ int bytesRead = stream.Read(this.buffer, 0, 9);
if (bytesRead != 9)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
@@ -251,9 +299,10 @@ private void ReadImageDescriptor()
///
/// Reads the logical screen descriptor.
///
- private void ReadLogicalScreenDescriptor()
+ /// The containing image data.
+ private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 7);
+ int bytesRead = stream.Read(this.buffer, 0, 7);
if (bytesRead != 7)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
@@ -266,103 +315,110 @@ private void ReadLogicalScreenDescriptor()
/// Reads the application extension block parsing any animation or XMP information
/// if present.
///
- private void ReadApplicationExtension()
+ /// The containing image data.
+ private void ReadApplicationExtension(BufferedReadStream stream)
{
- int appLength = this.stream.ReadByte();
+ int appLength = stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
+ long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize)
{
- this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
+ stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
-
- if (isXmp && !this.IgnoreMetadata)
+ if (isXmp && !this.skipMetadata)
{
- var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator);
+ GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
- this.metadata.XmpProfile = new XmpProfile(extension.Data);
+ this.metadata!.XmpProfile = new XmpProfile(extension.Data);
+ }
+ else
+ {
+ // Reset the stream position and continue.
+ stream.Position = position;
+ SkipBlock(stream, appLength);
}
return;
}
- else
- {
- int subBlockSize = this.stream.ReadByte();
- // TODO: There's also a NETSCAPE buffer extension.
- // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
- if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
- {
- this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
- this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
- this.stream.Skip(1); // Skip the terminator.
- return;
- }
+ int subBlockSize = stream.ReadByte();
- // Could be something else not supported yet.
- // Skip the subblock and terminator.
- this.SkipBlock(subBlockSize);
+ // TODO: There's also a NETSCAPE buffer extension.
+ // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
+ if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
+ {
+ stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
+ this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
+ stream.Skip(1); // Skip the terminator.
+ return;
}
+ // Could be something else not supported yet.
+ // Skip the subblock and terminator.
+ SkipBlock(stream, subBlockSize);
+
return;
}
- this.SkipBlock(appLength); // Not supported by any known decoder.
+ SkipBlock(stream, appLength); // Not supported by any known decoder.
}
///
/// Skips over a block or reads its terminator.
- /// The length of the block to skip.
///
- private void SkipBlock(int blockSize = 0)
+ /// The containing image data.
+ /// The length of the block to skip.
+ private static void SkipBlock(BufferedReadStream stream, int blockSize = 0)
{
if (blockSize > 0)
{
- this.stream.Skip(blockSize);
+ stream.Skip(blockSize);
}
int flag;
- while ((flag = this.stream.ReadByte()) > 0)
+ while ((flag = stream.ReadByte()) > 0)
{
- this.stream.Skip(flag);
+ stream.Skip(flag);
}
}
///
/// Reads the gif comments.
///
- private void ReadComments()
+ /// The containing image data.
+ private void ReadComments(BufferedReadStream stream)
{
int length;
- var stringBuilder = new StringBuilder();
- while ((length = this.stream.ReadByte()) != 0)
+ StringBuilder stringBuilder = new();
+ while ((length = stream.ReadByte()) != 0)
{
if (length > GifConstants.MaxCommentSubBlockLength)
{
GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
}
- if (this.IgnoreMetadata)
+ if (this.skipMetadata)
{
- this.stream.Seek(length, SeekOrigin.Current);
+ stream.Seek(length, SeekOrigin.Current);
continue;
}
- using IMemoryOwner commentsBuffer = this.MemoryAllocator.Allocate(length);
+ using IMemoryOwner commentsBuffer = this.memoryAllocator.Allocate(length);
Span commentsSpan = commentsBuffer.GetSpan();
- this.stream.Read(commentsSpan);
+ stream.Read(commentsSpan);
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart);
}
if (stringBuilder.Length > 0)
{
- this.gifMetadata.Comments.Add(stringBuilder.ToString());
+ this.gifMetadata!.Comments.Add(stringBuilder.ToString());
}
}
@@ -370,93 +426,77 @@ private void ReadComments()
/// Reads an individual gif frame.
///
/// The pixel format.
+ /// The containing image data.
/// The image to decode the information to.
/// The previous frame.
- private void ReadFrame(ref Image image, ref ImageFrame previousFrame)
+ private void ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame)
where TPixel : unmanaged, IPixel
{
- this.ReadImageDescriptor();
+ this.ReadImageDescriptor(stream);
- IMemoryOwner localColorTable = null;
- Buffer2D indices = null;
- try
- {
- // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
- if (this.imageDescriptor.LocalColorTableFlag)
- {
- int length = this.imageDescriptor.LocalColorTableSize * 3;
- localColorTable = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean);
- this.stream.Read(localColorTable.GetSpan());
- }
+ // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
+ bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
- indices = this.Configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
- this.ReadFrameIndices(indices);
-
- Span rawColorTable = default;
- if (localColorTable != null)
- {
- rawColorTable = localColorTable.GetSpan();
- }
- else if (this.globalColorTable != null)
- {
- rawColorTable = this.globalColorTable.GetSpan();
- }
-
- ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable);
- this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor);
+ if (hasLocalColorTable)
+ {
+ // Read and store the local color table. We allocate the maximum possible size and slice to match.
+ int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
+ this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length));
+ }
- // Skip any remaining blocks
- this.SkipBlock();
+ Span rawColorTable = default;
+ if (hasLocalColorTable)
+ {
+ rawColorTable = this.currentLocalColorTable!.GetSpan().Slice(0, this.currentLocalColorTableSize);
}
- finally
+ else if (this.globalColorTable != null)
{
- localColorTable?.Dispose();
- indices?.Dispose();
+ rawColorTable = this.globalColorTable.GetSpan();
}
- }
- ///
- /// Reads the frame indices marking the color to use for each pixel.
- ///
- /// The 2D pixel buffer to write to.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ReadFrameIndices(Buffer2D indices)
- {
- int minCodeSize = this.stream.ReadByte();
- using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream);
- lzwDecoder.DecodePixels(minCodeSize, indices);
+ ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable);
+ this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
+
+ // Skip any remaining blocks
+ SkipBlock(stream);
}
///
/// Reads the frames colors, mapping indices to colors.
///
/// The pixel format.
+ /// The containing image data.
/// The image to decode the information to.
/// The previous frame.
- /// The indexed pixels.
/// The color table containing the available colors.
/// The
- private void ReadFrameColors(ref Image image, ref ImageFrame previousFrame, Buffer2D indices, ReadOnlySpan colorTable, in GifImageDescriptor descriptor)
+ private void ReadFrameColors(
+ BufferedReadStream stream,
+ ref Image? image,
+ ref ImageFrame? previousFrame,
+ ReadOnlySpan colorTable,
+ in GifImageDescriptor descriptor)
where TPixel : unmanaged, IPixel
{
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
- ImageFrame prevFrame = null;
- ImageFrame currentFrame = null;
+ ImageFrame? prevFrame = null;
+ ImageFrame? currentFrame = null;
ImageFrame imageFrame;
if (previousFrame is null)
{
if (!transFlag)
{
- image = new Image(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata);
+ image = new Image(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
- image = new Image(this.Configuration, imageWidth, imageHeight, this.metadata);
+ image = new Image(this.configuration, imageWidth, imageHeight, this.metadata);
}
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
@@ -470,7 +510,10 @@ private void ReadFrameColors(ref Image image, ref ImageFrame(ref Image image, ref ImageFrame indicesRowOwner = this.memoryAllocator.Allocate(descriptor.Width);
+ Span indicesRow = indicesRowOwner.Memory.Span;
+ ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow);
+
+ int minCodeSize = stream.ReadByte();
+ if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
{
- ref byte indicesRowRef = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y - descriptorTop));
+ using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
- // Check if this image is interlaced.
- int writeY; // the target y offset to write to
- if (descriptor.InterlaceFlag)
+ for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++)
{
- // If so then we read lines at predetermined offsets.
- // When an entire image height worth of offset lines has been read we consider this a pass.
- // With each pass the number of offset lines changes and the starting line changes.
- if (interlaceY >= descriptor.Height)
+ // Check if this image is interlaced.
+ int writeY; // the target y offset to write to
+ if (descriptor.InterlaceFlag)
{
- interlacePass++;
- switch (interlacePass)
+ // If so then we read lines at predetermined offsets.
+ // When an entire image height worth of offset lines has been read we consider this a pass.
+ // With each pass the number of offset lines changes and the starting line changes.
+ if (interlaceY >= descriptor.Height)
{
- case 1:
- interlaceY = 4;
- break;
- case 2:
- interlaceY = 2;
- interlaceIncrement = 4;
- break;
- case 3:
- interlaceY = 1;
- interlaceIncrement = 2;
- break;
+ interlacePass++;
+ switch (interlacePass)
+ {
+ case 1:
+ interlaceY = 4;
+ break;
+ case 2:
+ interlaceY = 2;
+ interlaceIncrement = 4;
+ break;
+ case 3:
+ interlaceY = 1;
+ interlaceIncrement = 2;
+ break;
+ }
}
- }
- writeY = interlaceY + descriptor.Top;
- interlaceY += interlaceIncrement;
- }
- else
- {
- writeY = y;
- }
+ writeY = Math.Min(interlaceY + descriptor.Top, image.Height);
+ interlaceY += interlaceIncrement;
+ }
+ else
+ {
+ writeY = y;
+ }
- ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
+ lzwDecoder.DecodePixelRow(indicesRow);
+ ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
- if (!transFlag)
- {
- // #403 The left + width value can be larger than the image width
- for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ if (!transFlag)
{
- int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
- ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
- Rgb24 rgb = colorTable[index];
- pixel.FromRgb24(rgb);
+ // #403 The left + width value can be larger than the image width
+ for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ {
+ int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
+ ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
+ Rgb24 rgb = colorTable[index];
+ pixel.FromRgb24(rgb);
+ }
}
- }
- else
- {
- for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ else
{
- int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
- if (transIndex != index)
+ for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{
+ int index = Unsafe.Add(ref indicesRowRef, x - descriptorLeft);
+
+ // Treat any out of bounds values as transparent.
+ if (index > colorTableMaxIdx || index == transIndex)
+ {
+ continue;
+ }
+
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
@@ -574,6 +632,43 @@ private void ReadFrameColors(ref Image image, ref ImageFrame
+ /// Reads the frames metadata.
+ ///
+ /// The containing image data.
+ /// The collection of frame metadata.
+ /// The previous frame metadata.
+ private void ReadFrameMetadata(BufferedReadStream stream, List frameMetadata, ref ImageFrameMetadata? previousFrame)
+ {
+ this.ReadImageDescriptor(stream);
+
+ // Skip the color table for this frame if local.
+ if (this.imageDescriptor.LocalColorTableFlag)
+ {
+ // Read and store the local color table. We allocate the maximum possible size and slice to match.
+ int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
+ this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length));
+ }
+
+ // Skip the frame indices. Pixels length + mincode size.
+ // The gif format does not tell us the length of the compressed data beforehand.
+ int minCodeSize = stream.ReadByte();
+ if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
+ {
+ using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
+ lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height);
+ }
+
+ ImageFrameMetadata currentFrame = new();
+ frameMetadata.Add(currentFrame);
+ this.SetFrameMetadata(currentFrame);
+ previousFrame = currentFrame;
+
+ // Skip any remaining blocks
+ SkipBlock(stream);
+ }
+
///
/// Restores the current frame area to the background.
///
@@ -587,7 +682,7 @@ private void RestoreToBackground(ImageFrame frame)
return;
}
- var interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
+ Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
@@ -595,7 +690,7 @@ private void RestoreToBackground(ImageFrame frame)
}
///
- /// Sets the frames metadata.
+ /// Sets the metadata for the image frame.
///
/// The metadata.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -628,13 +723,11 @@ private void SetFrameMetadata(ImageFrameMetadata meta)
/// The stream containing image data.
private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{
- this.stream = stream;
-
// Skip the identifier
- this.stream.Skip(6);
- this.ReadLogicalScreenDescriptor();
+ stream.Skip(6);
+ this.ReadLogicalScreenDescriptor(stream);
- var meta = new ImageMetadata();
+ ImageMetadata meta = new();
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@@ -671,16 +764,26 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
- this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
-
if (globalColorTableLength > 0)
{
- this.globalColorTable = this.MemoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean);
+ this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean);
- // Read the global color table data from the stream
- stream.Read(this.globalColorTable.GetSpan());
+ // Read the global color table data from the stream and preserve it in the gif metadata
+ Span globalColorTableSpan = this.globalColorTable.GetSpan();
+ stream.Read(globalColorTableSpan);
+
+ //Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
+ //ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan);
+ //for (int i = 0; i < colorTable.Length; i++)
+ //{
+ // colorTable[i] = new Color(rgbTable[i]);
+ //}
+
+ //this.gifMetadata.GlobalColorTable = colorTable;
}
}
+
+ //this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
}
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
index b85bb139ad..ce6d14b44a 100644
--- a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
+++ b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Gif
@@ -24,5 +25,11 @@ public static void ThrowInvalidImageContentException(string errorMessage)
/// if no inner exception is specified.
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException);
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data");
}
}
diff --git a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
index 56bb6d6519..98145e0554 100644
--- a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
@@ -19,5 +19,10 @@ internal interface IGifDecoderOptions
/// Gets the decoding mode for multi-frame images.
///
FrameDecodingMode DecodingMode { get; }
+
+ ///
+ /// Gets or sets the maximum number of gif frames.
+ ///
+ uint MaxFrames { get; set; }
}
}
diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
index 77c769832d..aa4327a1a0 100644
--- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
@@ -45,10 +45,29 @@ internal sealed class LzwDecoder : IDisposable
///
private readonly IMemoryOwner suffix;
+ ///
+ /// The scratch buffer for reading data blocks.
+ ///
+ private readonly IMemoryOwner scratchBuffer;
+
///
/// The pixel stack buffer.
///
private readonly IMemoryOwner pixelStack;
+ private readonly int minCodeSize;
+ private readonly int clearCode;
+ private readonly int endCode;
+ private int code;
+ private int codeSize;
+ private int codeMask;
+ private int availableCode;
+ private int oldCode = NullCode;
+ private int bits;
+ private int top;
+ private int count;
+ private int bufferIndex;
+ private int data;
+ private int first;
///
/// Initializes a new instance of the class
@@ -56,185 +75,280 @@ internal sealed class LzwDecoder : IDisposable
///
/// The to use for buffer allocations.
/// The stream to read from.
+ /// The minimum code size.
/// is null.
- public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
+ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean);
+ this.scratchBuffer = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None);
+ this.minCodeSize = minCodeSize;
+
+ // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
+ this.clearCode = 1 << minCodeSize;
+ this.codeSize = minCodeSize + 1;
+ this.codeMask = (1 << this.codeSize) - 1;
+ this.endCode = this.clearCode + 1;
+ this.availableCode = this.clearCode + 2;
+
+ ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
+ for (this.code = 0; this.code < this.clearCode; this.code++)
+ {
+ Unsafe.Add(ref suffixRef, this.code) = (byte)this.code;
+ }
}
///
- /// Decodes and decompresses all pixel indices from the stream.
+ /// Gets a value indicating whether the minimum code size is valid.
///
- /// Minimum code size of the data.
- /// The pixel array to decode to.
- public void DecodePixels(int minCodeSize, Buffer2D pixels)
+ /// The minimum code size.
+ ///
+ /// if the minimum code size is valid; otherwise, .
+ ///
+ public static bool IsValidMinCodeSize(int minCodeSize)
{
- // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
- int clearCode = 1 << minCodeSize;
-
// It is possible to specify a larger LZW minimum code size than the palette length in bits
// which may leave a gap in the codes where no colors are assigned.
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
+ int clearCode = 1 << minCodeSize;
if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize)
{
// Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided
// color palette but we won't bother since the image is most likely corrupted.
- return;
+ return false;
}
- // The resulting index table length.
- int width = pixels.Width;
- int height = pixels.Height;
- int length = width * height;
+ return true;
+ }
- int codeSize = minCodeSize + 1;
+ ///
+ /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer.
+ ///
+ /// The pixel indices array to decode to.
+ public void DecodePixelRow(Span indices)
+ {
+ indices.Clear();
- // Calculate the end code
- int endCode = clearCode + 1;
+ ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices);
+ ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
+ ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
+ ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
+ Span buffer = this.scratchBuffer.GetSpan();
- // Calculate the available code.
- int availableCode = clearCode + 2;
+ int x = 0;
+ int xyz = 0;
+ while (xyz < indices.Length)
+ {
+ if (this.top == 0)
+ {
+ if (this.bits < this.codeSize)
+ {
+ // Load bytes until there are enough bits for a code.
+ if (this.count == 0)
+ {
+ // Read a new data block.
+ this.count = this.ReadBlock(buffer);
+ if (this.count == 0)
+ {
+ break;
+ }
- // Jillzhangs Code see: http://giflib.codeplex.com/
- // Adapted from John Cristy's ImageMagick.
- int code;
- int oldCode = NullCode;
- int codeMask = (1 << codeSize) - 1;
- int bits = 0;
+ this.bufferIndex = 0;
+ }
- int top = 0;
- int count = 0;
- int bi = 0;
- int xyz = 0;
+ this.data += buffer[this.bufferIndex] << this.bits;
- int data = 0;
- int first = 0;
+ this.bits += 8;
+ this.bufferIndex++;
+ this.count--;
+ continue;
+ }
- ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
- ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
- ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
+ // Get the next code
+ this.code = this.data & this.codeMask;
+ this.data >>= this.codeSize;
+ this.bits -= this.codeSize;
- for (code = 0; code < clearCode; code++)
- {
- Unsafe.Add(ref suffixRef, code) = (byte)code;
+ // Interpret the code
+ if (this.code > this.availableCode || this.code == this.endCode)
+ {
+ break;
+ }
+
+ if (this.code == this.clearCode)
+ {
+ // Reset the decoder
+ this.codeSize = this.minCodeSize + 1;
+ this.codeMask = (1 << this.codeSize) - 1;
+ this.availableCode = this.clearCode + 2;
+ this.oldCode = NullCode;
+ continue;
+ }
+
+ if (this.oldCode == NullCode)
+ {
+ Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code);
+ this.oldCode = this.code;
+ this.first = this.code;
+ continue;
+ }
+
+ int inCode = this.code;
+ if (this.code == this.availableCode)
+ {
+ Unsafe.Add(ref pixelStackRef, this.top++) = (byte)this.first;
+
+ this.code = this.oldCode;
+ }
+
+ while (this.code > this.clearCode)
+ {
+ Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code);
+ this.code = Unsafe.Add(ref prefixRef, this.code);
+ }
+
+ int suffixCode = Unsafe.Add(ref suffixRef, this.code);
+ this.first = suffixCode;
+ Unsafe.Add(ref pixelStackRef, this.top++) = suffixCode;
+
+ // Fix for Gifs that have "deferred clear code" as per here :
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=55918
+ if (this.availableCode < MaxStackSize)
+ {
+ Unsafe.Add(ref prefixRef, this.availableCode) = this.oldCode;
+ Unsafe.Add(ref suffixRef, this.availableCode) = this.first;
+ this.availableCode++;
+ if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
+ {
+ this.codeSize++;
+ this.codeMask = (1 << this.codeSize) - 1;
+ }
+ }
+
+ this.oldCode = inCode;
+ }
+
+ // Pop a pixel off the pixel stack.
+ this.top--;
+
+ // Clear missing pixels
+ xyz++;
+ Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, this.top);
}
+ }
- Span buffer = stackalloc byte[byte.MaxValue];
+ ///
+ /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data.
+ ///
+ /// The resulting index table length.
+ public void SkipIndices(int length)
+ {
+ ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
+ ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
+ ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
+ Span buffer = this.scratchBuffer.GetSpan();
- int y = 0;
- int x = 0;
- int rowMax = width;
- ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y));
+ int xyz = 0;
while (xyz < length)
{
- // Reset row reference.
- if (xyz == rowMax)
- {
- x = 0;
- pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y));
- rowMax = (y * width) + width;
- }
-
- if (top == 0)
+ if (this.top == 0)
{
- if (bits < codeSize)
+ if (this.bits < this.codeSize)
{
// Load bytes until there are enough bits for a code.
- if (count == 0)
+ if (this.count == 0)
{
// Read a new data block.
- count = this.ReadBlock(buffer);
- if (count == 0)
+ this.count = this.ReadBlock(buffer);
+ if (this.count == 0)
{
break;
}
- bi = 0;
+ this.bufferIndex = 0;
}
- data += buffer[bi] << bits;
+ this.data += buffer[this.bufferIndex] << this.bits;
- bits += 8;
- bi++;
- count--;
+ this.bits += 8;
+ this.bufferIndex++;
+ this.count--;
continue;
}
// Get the next code
- code = data & codeMask;
- data >>= codeSize;
- bits -= codeSize;
+ this.code = this.data & this.codeMask;
+ this.data >>= this.codeSize;
+ this.bits -= this.codeSize;
// Interpret the code
- if (code > availableCode || code == endCode)
+ if (this.code > this.availableCode || this.code == this.endCode)
{
break;
}
- if (code == clearCode)
+ if (this.code == this.clearCode)
{
// Reset the decoder
- codeSize = minCodeSize + 1;
- codeMask = (1 << codeSize) - 1;
- availableCode = clearCode + 2;
- oldCode = NullCode;
+ this.codeSize = this.minCodeSize + 1;
+ this.codeMask = (1 << this.codeSize) - 1;
+ this.availableCode = this.clearCode + 2;
+ this.oldCode = NullCode;
continue;
}
- if (oldCode == NullCode)
+ if (this.oldCode == NullCode)
{
- Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
- oldCode = code;
- first = code;
+ Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code);
+ this.oldCode = this.code;
+ this.first = this.code;
continue;
}
- int inCode = code;
- if (code == availableCode)
+ int inCode = this.code;
+ if (this.code == this.availableCode)
{
- Unsafe.Add(ref pixelStackRef, top++) = (byte)first;
+ Unsafe.Add(ref pixelStackRef, this.top++) = (byte)this.first;
- code = oldCode;
+ this.code = this.oldCode;
}
- while (code > clearCode)
+ while (this.code > this.clearCode)
{
- Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
- code = Unsafe.Add(ref prefixRef, code);
+ Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code);
+ this.code = Unsafe.Add(ref prefixRef, this.code);
}
- int suffixCode = Unsafe.Add(ref suffixRef, code);
- first = suffixCode;
- Unsafe.Add(ref pixelStackRef, top++) = suffixCode;
+ int suffixCode = Unsafe.Add(ref suffixRef, this.code);
+ this.first = suffixCode;
+ Unsafe.Add(ref pixelStackRef, this.top++) = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here :
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
- if (availableCode < MaxStackSize)
+ if (this.availableCode < MaxStackSize)
{
- Unsafe.Add(ref prefixRef, availableCode) = oldCode;
- Unsafe.Add(ref suffixRef, availableCode) = first;
- availableCode++;
- if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
+ Unsafe.Add(ref prefixRef, this.availableCode) = this.oldCode;
+ Unsafe.Add(ref suffixRef, this.availableCode) = this.first;
+ this.availableCode++;
+ if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
{
- codeSize++;
- codeMask = (1 << codeSize) - 1;
+ this.codeSize++;
+ this.codeMask = (1 << this.codeSize) - 1;
}
}
- oldCode = inCode;
+ this.oldCode = inCode;
}
// Pop a pixel off the pixel stack.
- top--;
+ this.top--;
// Clear missing pixels
xyz++;
- Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, top);
}
}
@@ -267,6 +381,7 @@ public void Dispose()
this.prefix.Dispose();
this.suffix.Dispose();
this.pixelStack.Dispose();
+ this.scratchBuffer.Dispose();
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
index eab5e6a082..fc35f82b7a 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
@@ -147,7 +147,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target
quality = (int)Math.Round(5000.0 / sumPercent);
}
- return quality;
+ return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor);
}
///
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index 8f01500dd0..9ecf8ad502 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -183,6 +183,17 @@ public void Issue1530_BadDescriptorDimensions(TestImageProvider
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
+ // https://github.com/SixLabors/ImageSharp/issues/2758
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)]
+ public void Issue2758_BadDescriptorDimensions(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
+ }
+
// https://github.com/SixLabors/ImageSharp/issues/405
[Theory]
[WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
index d9915f17d6..d662a23bcc 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
@@ -10,6 +10,7 @@
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using Xunit;
@@ -364,6 +365,31 @@ public void EncodedStringTags_Read()
}
}
+ [Theory(Skip = "2.1 JPEG decoder detects this image as invalid.")]
+ [WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)]
+ public void Issue2758_DecodeWorks(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+
+ Assert.Equal(59787, image.Width);
+ Assert.Equal(511, image.Height);
+
+ JpegMetadata meta = image.Metadata.GetJpegMetadata();
+
+ // Quality determination should be between 1-100.
+ Assert.Equal(15, meta.LuminanceQuality);
+ Assert.Equal(1, meta.ChrominanceQuality);
+
+ // We want to test the encoder to ensure the determined values can be encoded but not by encoding
+ // the full size image as it would be too slow.
+ // We will crop the image to a smaller size and then encode it.
+ image.Mutate(x => x.Crop(new(0, 0, 100, 100)));
+
+ using MemoryStream ms = new();
+ image.Save(ms, new JpegEncoder());
+ }
+
private static void VerifyEncodedStrings(ExifProfile exif)
{
Assert.NotNull(exif);
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 0cee1715e8..1ba9ef2ef3 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -269,6 +269,7 @@ public static class Issues
public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg";
public const string Issue2133DeduceColorSpace = "Jpg/issues/Issue2133.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
+ public const string Issue2758 = "Jpg/issues/issue-2758.jpg";
public static class Fuzz
{
@@ -463,6 +464,7 @@ public static class Issues
public const string Issue1962NoColorTable = "Gif/issues/issue1962_tiniest_gif_1st.gif";
public const string Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif";
public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif";
+ public const string Issue2758 = "Gif/issues/issue_2758.gif";
}
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png
index cdba9277b1..b07e806620 100644
--- a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a3a24c066895fd3a76649da376485cbc1912d6a3ae15369575f523e66364b3b6
-size 141563
+oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70
+size 142244
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png
new file mode 100644
index 0000000000..f63cc98ada
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38
+size 81944
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png
new file mode 100644
index 0000000000..f63cc98ada
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38
+size 81944
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png
index 75cf685e43..efba40c99d 100644
--- a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:489642f0c81fd12e97007fe6feb11b0e93e351199a922ce038069a3782ad0722
-size 135
+oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309
+size 126
diff --git a/tests/Images/Input/Gif/issues/issue_2758.gif b/tests/Images/Input/Gif/issues/issue_2758.gif
new file mode 100644
index 0000000000..17db9fa132
--- /dev/null
+++ b/tests/Images/Input/Gif/issues/issue_2758.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13e9374181c7536d1d2ecb514753a5290c0ec06234ca079c6c8c8a832586b668
+size 199
diff --git a/tests/Images/Input/Jpg/issues/issue-2758.jpg b/tests/Images/Input/Jpg/issues/issue-2758.jpg
new file mode 100644
index 0000000000..48ee1159ec
--- /dev/null
+++ b/tests/Images/Input/Jpg/issues/issue-2758.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f32a238b57b7073f7442f8ae7efd6ba3ae4cda30d57e6666fb8a1eaa27108558
+size 1412