Skip to content
103 changes: 66 additions & 37 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
}

this.ReadFrame(ref image, ref previousFrame);

// Reset per-frame state.
this.imageDescriptor = default;
this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
Expand Down Expand Up @@ -161,6 +165,11 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
this.globalColorTable?.Dispose();
}

if (image is null)
{
GifThrowHelper.ThrowNoData();
}

return image;
}

Expand Down Expand Up @@ -214,6 +223,11 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
this.globalColorTable?.Dispose();
}

if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
{
GifThrowHelper.ThrowNoHeader();
}

return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
this.logicalScreenDescriptor.Width,
Expand Down Expand Up @@ -277,40 +291,44 @@ private void ReadApplicationExtension()

// 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 = this.stream.Position;
if (appLength == GifConstants.ApplicationBlockSize)
{
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);

if (isXmp && !this.skipMetadata)
{
var extension = GifXmpApplicationExtension.Read(this.stream, this.memoryAllocator);
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(this.stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata.XmpProfile = new XmpProfile(extension.Data);
}
else
{
// Reset the stream position and continue.
this.stream.Position = position;
this.SkipBlock(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 = this.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)
{
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;
}

// Could be something else not supported yet.
// Skip the subblock and terminator.
this.SkipBlock(subBlockSize);

return;
}

Expand Down Expand Up @@ -464,7 +482,7 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
}

this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata, true);

imageFrame = image.Frames.RootFrame;
}
Expand All @@ -475,9 +493,9 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi
prevFrame = previousFrame;
}

currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection
currentFrame = image.Frames.CreateFrame();

this.SetFrameMetadata(currentFrame.Metadata);
this.SetFrameMetadata(currentFrame.Metadata, false);

imageFrame = currentFrame;

Expand Down Expand Up @@ -554,13 +572,18 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi
{
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
if (transIndex != index)
int rawIndex = Unsafe.Add(ref indicesRowRef, x - descriptorLeft);

// Treat any out of bounds values as transparent.
if (rawIndex > colorTableMaxIdx || rawIndex == transIndex)
{
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
continue;
}

int index = Numerics.Clamp(rawIndex, 0, colorTableMaxIdx);
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
}
}
}
Expand Down Expand Up @@ -592,7 +615,7 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
return;
}

var interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();

Expand All @@ -603,28 +626,34 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
/// Sets the frames metadata.
/// </summary>
/// <param name="meta">The metadata.</param>
/// <param name="isRoot">Whether the metadata represents the root frame.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFrameMetadata(ImageFrameMetadata meta)
private void SetFrameMetadata(ImageFrameMetadata meta, bool isRoot)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
if (this.graphicsControlExtension.DelayTime > 0)
{
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
}

// Frames can either use the global table or their own local table.
if (this.logicalScreenDescriptor.GlobalColorTableFlag
if (isRoot && this.logicalScreenDescriptor.GlobalColorTableFlag
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
else if (this.imageDescriptor.LocalColorTableFlag

if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
}

gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}
}

/// <summary>
Expand All @@ -639,7 +668,7 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
this.stream.Skip(6);
this.ReadLogicalScreenDescriptor();

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
Expand Down
128 changes: 64 additions & 64 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken

// Quantize the image returning a palette.
IndexedImageFrame<TPixel> quantized;

using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
if (useGlobalTable)
Expand Down Expand Up @@ -133,101 +132,102 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}

if (useGlobalTable)
{
this.EncodeGlobal(image, quantized, index, stream);
}
else
{
this.EncodeLocal(image, quantized, stream);
}

// Clean up.
quantized.Dispose();
this.EncodeFrames(stream, image, quantized, quantized.Palette.ToArray());

stream.WriteByte(GifConstants.EndIntroducer);
}

private void EncodeGlobal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, int transparencyIndex, Stream stream)
private void EncodeFrames<TPixel>(
Stream stream,
Image<TPixel> image,
IndexedImageFrame<TPixel> quantized,
ReadOnlyMemory<TPixel> palette)
where TPixel : unmanaged, IPixel<TPixel>
{
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
PaletteQuantizer<TPixel> paletteFrameQuantizer = default;
bool quantizerInitialized = false;
PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false;
for (int i = 0; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
this.WriteGraphicalControlExtension(frameMetadata, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream);
bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata frameMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata.ColorTableMode == GifColorTableMode.Local);

if (i == 0)
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
this.WriteImageData(quantized, stream);
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
hasPaletteQuantizer = true;
paletteQuantizer = new(this.configuration, this.quantizer.Options, palette);
}
else
{
if (!quantizerInitialized)
{
quantizerInitialized = true;
paletteFrameQuantizer = new PaletteQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette);
}

using IndexedImageFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized, ref paletteQuantizer);

// Clean up for the next run.
quantized.Dispose();
quantized = null;
}

paletteFrameQuantizer.Dispose();
paletteQuantizer.Dispose();
}

private void EncodeLocal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, Stream stream)
private void EncodeFrame<TPixel>(
Stream stream,
ImageFrame<TPixel> frame,
int frameIndex,
bool useLocal,
GifFrameMetadata metadata,
ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> paletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = null;
for (int i = 0; i < image.Frames.Count; i++)
// The first frame has already been quantized so we do not need to do so again.
if (frameIndex > 0)
{
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
if (useLocal)
{
// Allow each frame to be encoded at whatever color depth the frame designates if set.
if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength
&& frameMetadata.ColorTableLength > 0)
// Reassign using the current frame and details.
QuantizerOptions options = null;
int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
{
QuantizerOptions options = new()
options = new()
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = frameMetadata.ColorTableLength
MaxColors = colorTableLength
};

using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}
else
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}

using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options ?? this.quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}
else
{
// Quantize the image using the global palette.
quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
}

this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteGraphicalControlExtension(frameMetadata, GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
}

quantized.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
// Do we have extension information to write?
int index = GetTransparentIndex(quantized);
if (metadata != null || index > -1)
{
this.WriteGraphicalControlExtension(metadata ?? new(), index, stream);
}

this.WriteImageDescriptor(frame, useLocal, stream);

if (useLocal)
{
this.WriteColorTable(quantized, stream);
}

this.WriteImageData(quantized, stream);
}

/// <summary>
Expand Down Expand Up @@ -407,7 +407,7 @@ private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan<char> comme
}

/// <summary>
/// Writes the graphics control extension to the stream.
/// Writes the optional graphics control extension to the stream.
/// </summary>
/// <param name="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
Expand Down
Loading