📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

999 lines
38 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Performs the gif decoding operation.
/// </summary>
internal sealed class GifDecoderCore : ImageDecoderCore
{
/// <summary>
/// The temp buffer used to reduce allocations.
/// </summary>
private InlineArray16<byte> buffer; // mutable struct, don't make readonly
/// <summary>
/// The global color table.
/// </summary>
private IMemoryOwner<byte>? globalColorTable;
/// <summary>
/// The current local color table.
/// </summary>
private IMemoryOwner<byte>? currentLocalColorTable;
/// <summary>
/// Gets the size in bytes of the current local color table.
/// </summary>
private int currentLocalColorTableSize;
/// <summary>
/// The area to restore.
/// </summary>
private Rectangle? restoreArea;
/// <summary>
/// The logical screen descriptor.
/// </summary>
private GifLogicalScreenDescriptor logicalScreenDescriptor;
/// <summary>
/// The graphics control extension.
/// </summary>
private GifGraphicControlExtension graphicsControlExtension;
/// <summary>
/// The image descriptor.
/// </summary>
private GifImageDescriptor imageDescriptor;
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The maximum number of frames to decode. Inclusive.
/// </summary>
private readonly uint maxFrames;
/// <summary>
/// Whether to skip metadata during decode.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// The abstract metadata.
/// </summary>
private ImageMetadata? metadata;
/// <summary>
/// The gif specific metadata.
/// </summary>
private GifMetadata? gifMetadata;
/// <summary>
/// The background color index.
/// </summary>
private byte backgroundColorIndex;
/// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary>
/// <param name="options">The decoder options.</param>
public GifDecoderCore(DecoderOptions options)
: base(options)
{
this.configuration = options.Configuration;
this.skipMetadata = options.SkipMetadata;
this.maxFrames = options.MaxFrames;
this.memoryAllocator = this.configuration.MemoryAllocator;
}
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
Image<TPixel>? image = null;
ImageFrame<TPixel>? previousFrame = null;
FrameDisposalMode? previousDisposalMode = null;
bool globalColorTableUsed = false;
Color backgroundColor = Color.Transparent;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
// Loop though the respective gif parts and read the data.
int nextFlag = stream.ReadByte();
while (nextFlag != GifConstants.Terminator)
{
if (nextFlag == GifConstants.ImageLabel)
{
if (previousFrame != null && ++frameCount == this.maxFrames)
{
break;
}
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMode, ref backgroundColor);
// Reset per-frame state.
this.imageDescriptor = default;
this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
else if (nextFlag == GifConstants.EndIntroducer)
{
break;
}
nextFlag = stream.ReadByte();
if (nextFlag == -1)
{
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (image is null)
{
GifThrowHelper.ThrowNoData();
}
return image;
}
/// <inheritdoc />
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageFrameMetadata? previousFrame = null;
List<ImageFrameMetadata> framesMetadata = [];
bool globalColorTableUsed = false;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
// Loop though the respective gif parts and read the data.
int nextFlag = stream.ReadByte();
while (nextFlag != GifConstants.Terminator)
{
if (nextFlag == GifConstants.ImageLabel)
{
if (previousFrame != null && ++frameCount == this.maxFrames)
{
break;
}
globalColorTableUsed |= 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.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
else if (nextFlag == GifConstants.EndIntroducer)
{
break;
}
nextFlag = stream.ReadByte();
if (nextFlag == -1)
{
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
{
GifThrowHelper.ThrowNoHeader();
}
// Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data.
if (previousFrame is null)
{
GifThrowHelper.ThrowNoData();
}
return new ImageInfo(
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
framesMetadata);
}
/// <summary>
/// Reads the graphic control extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadGraphicalControlExtension(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer, 0, 6);
if (bytesRead != 6)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
}
this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer);
}
/// <summary>
/// Reads the image descriptor.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadImageDescriptor(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer, 0, 9);
if (bytesRead != 9)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
}
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0)
{
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
}
this.Dimensions = new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
}
/// <summary>
/// Reads the logical screen descriptor.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer, 0, 7);
if (bytesRead != 7)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
}
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer);
}
/// <summary>
/// Reads the application extension block parsing any animation or XMP information
/// if present.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadApplicationExtension(BufferedReadStream stream)
{
int appLength = stream.ReadByte();
if (appLength == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
if (appLength != GifConstants.ApplicationBlockSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid");
SkipBlock(stream, appLength);
return;
}
// 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;
int bytesRead = stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
if (bytesRead != GifConstants.ApplicationBlockSize)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
bool isXmp = ((ReadOnlySpan<byte>)this.buffer).StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp)
{
this.ReadXmpApplicationExtension(stream, position, appLength);
return;
}
int subBlockSize = stream.ReadByte();
if (subBlockSize == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.ReadNetscapeApplicationExtension(stream);
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
}
/// <summary>
/// Reads the GIF XMP application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength)
{
if (this.skipMetadata)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
return;
}
bool completed = false;
this.ExecuteAncillarySegmentAction(
() =>
{
this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength);
completed = true;
});
if (!completed)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
}
/// <summary>
/// Reads the GIF XMP application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
return;
}
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
/// <summary>
/// Reads the GIF NETSCAPE looping application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtension(BufferedReadStream stream) =>
this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream));
/// <summary>
/// Reads the GIF NETSCAPE looping application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer[1..]).RepeatCount;
int terminator = stream.ReadByte();
if (terminator == -1)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
}
/// <summary>
/// Skips over a block or reads its terminator.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="blockSize">The length of the block to skip.</param>
private static void SkipBlock(BufferedReadStream stream, int blockSize = 0)
{
if (blockSize > 0)
{
stream.Skip(blockSize);
}
int flag;
while ((flag = stream.ReadByte()) > 0)
{
stream.Skip(flag);
}
}
/// <summary>
/// Reads the gif comments.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadComments(BufferedReadStream stream)
{
int length;
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 (length == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
if (this.skipMetadata)
{
stream.Seek(length, SeekOrigin.Current);
continue;
}
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
Span<byte> commentsSpan = commentsBuffer.GetSpan();
int bytesRead = stream.Read(commentsSpan);
if (bytesRead != length)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart);
}
if (stringBuilder.Length > 0)
{
this.gifMetadata!.Comments.Add(stringBuilder.ToString());
}
}
/// <summary>
/// Reads an individual gif frame.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="backgroundColor">The background color.</param>
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref FrameDisposalMode? previousDisposalMode,
ref Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ReadImageDescriptor(stream);
// 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;
Span<byte> rawColorTable = default;
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<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
rawColorTable = this.currentLocalColorTable.GetSpan()[..length];
}
else if (this.globalColorTable != null)
{
rawColorTable = this.globalColorTable.GetSpan();
}
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
// First frame
if (image is null)
{
if (this.backgroundColorIndex < colorTable.Length)
{
backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]);
}
else
{
backgroundColor = Color.Transparent;
}
// We zero the alpha only when this frame declares transparency so that
// frames with a transparent index coalesce over a transparent canvas rather than
// baking the LSD background as a matte. When the flag is not set, this frame will
// write an opaque color for every addressed pixel; keeping the LSD background
// opaque here allows ReadFrameColors to show that background in uncovered areas
// for non-transparent GIFs that rely on it. We still do not prefill the canvas here.
if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
}
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundColor.ToPixel<TPixel>());
// Update from newly decoded frame.
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
if (disposalMethod != FrameDisposalMode.RestoreToPrevious)
{
// Do not key this on the transparency flag. Disposal handling is determined by
// the previous frame's disposal, not by whether the current frame declares a transparent
// index. For editing we carry a transparent background so that RestoreToBackground clears
// remove pixels to transparent rather than painting an opaque matte. The LSD background
// color is display advice and should be used only when explicitly flattening or when
// rendering with an option to honor it.
backgroundColor = (this.backgroundColorIndex < colorTable.Length)
? Color.FromPixel(colorTable[this.backgroundColorIndex]).WithAlpha(0)
: Color.Transparent;
}
// Skip any remaining blocks
SkipBlock(stream);
return !hasLocalColorTable;
}
/// <summary>
/// Reads the frames colors, mapping indices to colors.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="backgroundPixel">The background color pixel.</param>
private void ReadFrameColors<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref FrameDisposalMode? previousDisposalMode,
ReadOnlySpan<Rgb24> colorTable,
TPixel backgroundPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
GifImageDescriptor descriptor = this.imageDescriptor;
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool useTransparency = this.graphicsControlExtension.TransparencyFlag;
bool useBackground;
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame;
ImageFrame<TPixel>? restoreFrame = null;
if (previousFrame is null && previousDisposalMode is null)
{
// First frame: prefill with LSD background iff a GCT exists (policy: HonorBackgroundColor).
useBackground =
this.logicalScreenDescriptor.GlobalColorTableFlag
&& disposalMethod == FrameDisposalMode.RestoreToBackground;
image = useBackground
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
currentFrame = image.Frames.RootFrame;
}
else
{
// Subsequent frames: use LSD background iff previous disposal was RestoreToBackground and a GCT exists.
useBackground =
this.logicalScreenDescriptor.GlobalColorTableFlag
&& previousDisposalMode == FrameDisposalMode.RestoreToBackground;
if (previousFrame != null)
{
currentFrame = image!.Frames.AddFrame(previousFrame);
}
else if (useBackground)
{
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
}
else
{
currentFrame = image!.Frames.CreateFrame();
}
this.SetFrameMetadata(currentFrame.Metadata);
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
restoreFrame = previousFrame;
}
if (previousDisposalMode == FrameDisposalMode.RestoreToBackground)
{
this.RestoreToBackground(currentFrame, backgroundPixel, !useBackground);
}
}
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
previousFrame = restoreFrame;
}
else
{
previousFrame = currentFrame;
}
previousDisposalMode = disposalMethod;
if (disposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = Rectangle.Intersect(image.Bounds, new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
}
if (colorTable.Length == 0)
{
return;
}
int interlacePass = 0; // The interlace pass
int interlaceIncrement = 8; // The interlacing line increment
int interlaceY = 0; // The current interlaced line
int descriptorTop = descriptor.Top;
int descriptorBottom = descriptorTop + descriptor.Height;
int descriptorLeft = descriptor.Left;
int descriptorRight = descriptorLeft + descriptor.Width;
byte transIndex = this.graphicsControlExtension.TransparencyIndex;
int colorTableMaxIdx = colorTable.Length - 1;
// For a properly encoded gif the descriptor dimensions will never exceed the logical screen dimensions.
// However we have images that exceed this that can be decoded by other libraries. #1530
using IMemoryOwner<byte> indicesRowOwner = this.memoryAllocator.Allocate<byte>(descriptor.Width);
Span<byte> indicesRow = indicesRowOwner.Memory.Span;
int minCodeSize = stream.ReadByte();
if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
{
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++)
{
// Check if this image is interlaced.
int writeY; // the target y offset to write to
if (descriptor.InterlaceFlag)
{
// 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)
{
interlacePass++;
switch (interlacePass)
{
case 1:
interlaceY = 4;
break;
case 2:
interlaceY = 2;
interlaceIncrement = 4;
break;
case 3:
interlaceY = 1;
interlaceIncrement = 2;
break;
}
}
writeY = Math.Min(interlaceY + descriptor.Top, image.Height);
interlaceY += interlaceIncrement;
}
else
{
writeY = y;
}
lzwDecoder.DecodePixelRow(indicesRow);
// #403 The left + width value can be larger than the image width
int maxX = Math.Min(descriptorRight, imageWidth);
Span<TPixel> row = currentFrame.PixelBuffer.DangerousGetRowSpan(writeY);
// Take the descriptorLeft..maxX slice of the row, so the loop can be simplified.
row = row[descriptorLeft..maxX];
if (!useTransparency)
{
for (int x = 0; x < row.Length; x++)
{
int index = indicesRow[x];
// Treat any out of bounds values as background.
if (index > colorTableMaxIdx)
{
index = Numerics.Clamp(index, 0, colorTableMaxIdx);
}
row[x] = TPixel.FromRgb24(colorTable[index]);
}
}
else
{
for (int x = 0; x < row.Length; x++)
{
int index = indicesRow[x];
// Treat any out of bounds values as transparent.
// We explicitly set the pixel to transparent rather than alter the inbound
// color palette.
if (index > colorTableMaxIdx || index == transIndex)
{
continue;
}
row[x] = TPixel.FromRgb24(colorTable[index]);
}
}
}
}
}
/// <summary>
/// Reads the frames metadata.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="frameMetadata">The collection of frame metadata.</param>
/// <param name="previousFrame">The previous frame metadata.</param>
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> 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<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
else
{
this.currentLocalColorTable = null;
this.currentLocalColorTableSize = 0;
}
// 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);
return !this.imageDescriptor.LocalColorTableFlag;
}
/// <summary>
/// Restores the current frame area to the background.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The frame.</param>
/// <param name="background">The background color.</param>
/// <param name="transparent">Whether the background is transparent.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame, TPixel background, bool transparent)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.restoreArea is null)
{
return;
}
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
if (transparent)
{
pixelRegion.Clear();
}
else
{
pixelRegion.Fill(background);
}
this.restoreArea = null;
}
/// <summary>
/// Sets the metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFrameMetadata(ImageFrameMetadata metadata)
{
// Frames can either use the global table or their own local table.
if (this.logicalScreenDescriptor.GlobalColorTableFlag
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = FrameColorTableMode.Global;
}
if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = FrameColorTableMode.Local;
Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]);
Color.FromPixel(rgbTable, colorTable);
gifMeta.LocalColorTable = colorTable;
}
// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMode = this.graphicsControlExtension.DisposalMethod;
}
}
/// <summary>
/// Reads the logical screen descriptor and global color table blocks
/// </summary>
/// <param name="stream">The stream containing image data. </param>
[MemberNotNull(nameof(metadata))]
[MemberNotNull(nameof(gifMetadata))]
private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{
// Skip the identifier
stream.Skip(6);
this.ReadLogicalScreenDescriptor(stream);
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
// specification of the widest pixel of 4:1 to the tallest pixel of
// 1:4 in increments of 1/64th.
//
// Values : 0 - No aspect ratio information is given.
// 1..255 - Value used in the computation.
//
// Aspect Ratio = (Pixel Aspect Ratio + 15) / 64
if (this.logicalScreenDescriptor.PixelAspectRatio > 0)
{
meta.ResolutionUnits = PixelResolutionUnit.AspectRatio;
float ratio = (this.logicalScreenDescriptor.PixelAspectRatio + 15) / 64F;
if (ratio > 1)
{
meta.HorizontalResolution = ratio;
meta.VerticalResolution = 1;
}
else
{
meta.VerticalResolution = 1 / ratio;
meta.HorizontalResolution = 1;
}
}
this.metadata = meta;
this.gifMetadata = meta.GetGifMetadata();
this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? FrameColorTableMode.Global
: FrameColorTableMode.Local;
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
if (globalColorTableLength > 0)
{
this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean);
// Read the global color table data from the stream and preserve it in the gif metadata
Span<byte> globalColorTableSpan = this.globalColorTable.GetSpan();
stream.Read(globalColorTableSpan);
Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(globalColorTableSpan);
Color.FromPixel(rgbTable, colorTable);
this.gifMetadata.GlobalColorTable = colorTable;
}
}
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
this.backgroundColorIndex = index;
ReadOnlyMemory<Color>? globalColorTable = this.gifMetadata.GlobalColorTable;
if (globalColorTable.HasValue && index < globalColorTable.Value.Length)
{
this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index];
}
}
}