Browse Source

Merge pull request #2289 from SixLabors/defect/2288

Fix various Gif Decoder/Encoder behaviors.
pull/2237/merge
James Jackson-South 4 years ago
committed by GitHub
parent
commit
4a618a04e7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 103
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  2. 128
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 8
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  4. 2
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  5. 9
      src/ImageSharp/Formats/Gif/GifThrowHelper.cs
  6. 22
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  7. 29
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  8. 1
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  9. 8
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  10. 31
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  11. 2
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs
  12. 8
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  13. 38
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  14. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs
  15. 4
      tests/ImageSharp.Tests/TestImages.cs
  16. 3
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs
  17. 4
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs
  18. 7
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
  19. 9
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs
  20. 4
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs
  21. 3
      tests/Images/Input/Gif/issues/issue_2288.gif
  22. 3
      tests/Images/Input/Gif/issues/issue_2288_2.gif
  23. 3
      tests/Images/Input/Gif/issues/issue_2288_3.gif
  24. 3
      tests/Images/Input/Gif/issues/issue_2288_4.gif

103
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -125,6 +125,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
this.ReadFrame(ref image, ref previousFrame); this.ReadFrame(ref image, ref previousFrame);
// Reset per-frame state.
this.imageDescriptor = default;
this.graphicsControlExtension = default;
} }
else if (nextFlag == GifConstants.ExtensionIntroducer) else if (nextFlag == GifConstants.ExtensionIntroducer)
{ {
@ -161,6 +165,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
} }
if (image is null)
{
GifThrowHelper.ThrowNoData();
}
return image; return image;
} }
@ -214,6 +223,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
} }
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
{
GifThrowHelper.ThrowNoHeader();
}
return new ImageInfo( return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Width,
@ -277,40 +291,44 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// If the length is 11 then it's a valid extension and most likely // 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. // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = this.stream.Position;
if (appLength == GifConstants.ApplicationBlockSize) if (appLength == GifConstants.ApplicationBlockSize)
{ {
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp && !this.skipMetadata) 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) 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.
this.stream.Position = position;
this.SkipBlock(appLength);
}
return; return;
} }
else
{
int subBlockSize = this.stream.ReadByte();
// TODO: There's also a NETSCAPE buffer extension. int subBlockSize = this.stream.ReadByte();
// 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. // TODO: There's also a NETSCAPE buffer extension.
// Skip the subblock and terminator. // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
this.SkipBlock(subBlockSize); 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; return;
} }
@ -464,7 +482,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata); 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; imageFrame = image.Frames.RootFrame;
} }
@ -475,9 +493,9 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame; 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; imageFrame = currentFrame;
@ -554,13 +572,18 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{ {
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); int rawIndex = Unsafe.Add(ref indicesRowRef, x - descriptorLeft);
if (transIndex != index)
// Treat any out of bounds values as transparent.
if (rawIndex > colorTableMaxIdx || rawIndex == transIndex)
{ {
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); continue;
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
} }
int index = Numerics.Clamp(rawIndex, 0, colorTableMaxIdx);
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
} }
} }
} }
@ -592,7 +615,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
return; 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); Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear(); pixelRegion.Clear();
@ -603,28 +626,34 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// Sets the frames metadata. /// Sets the frames metadata.
/// </summary> /// </summary>
/// <param name="meta">The metadata.</param> /// <param name="meta">The metadata.</param>
/// <param name="isRoot">Whether the metadata represents the root frame.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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. // 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) && this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{ {
GifFrameMetadata gifMeta = meta.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
} }
else if (this.imageDescriptor.LocalColorTableFlag
if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0) && this.imageDescriptor.LocalColorTableSize > 0)
{ {
GifFrameMetadata gifMeta = meta.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; 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> /// <summary>
@ -639,7 +668,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.stream.Skip(6); this.stream.Skip(6);
this.ReadLogicalScreenDescriptor(); this.ReadLogicalScreenDescriptor();
var meta = new ImageMetadata(); ImageMetadata meta = new();
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's // 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 // width over its height. The value range in this field allows

128
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -93,7 +93,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Quantize the image returning a palette. // Quantize the image returning a palette.
IndexedImageFrame<TPixel> quantized; IndexedImageFrame<TPixel> quantized;
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration)) using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{ {
if (useGlobalTable) if (useGlobalTable)
@ -133,101 +132,102 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
} }
if (useGlobalTable) this.EncodeFrames(stream, image, quantized, quantized.Palette.ToArray());
{
this.EncodeGlobal(image, quantized, index, stream);
}
else
{
this.EncodeLocal(image, quantized, stream);
}
// Clean up.
quantized.Dispose();
stream.WriteByte(GifConstants.EndIntroducer); 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The palette quantizer can reuse the same pixel map across multiple frames PaletteQuantizer<TPixel> paletteQuantizer = default;
// since the palette is unchanging. This allows a reduction of memory usage across bool hasPaletteQuantizer = false;
// multi frame gifs using a global palette.
PaletteQuantizer<TPixel> paletteFrameQuantizer = default;
bool quantizerInitialized = false;
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
// Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata; ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata(); bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata frameMetadata);
this.WriteGraphicalControlExtension(frameMetadata, transparencyIndex, stream); bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata.ColorTableMode == GifColorTableMode.Local);
this.WriteImageDescriptor(frame, false, stream);
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.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized, ref paletteQuantizer);
this.WriteImageData(paletteQuantized, stream);
} // 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrame<TPixel> previousFrame = null; // The first frame has already been quantized so we do not need to do so again.
GifFrameMetadata previousMeta = null; if (frameIndex > 0)
for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i]; if (useLocal)
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
{ {
// Allow each frame to be encoded at whatever color depth the frame designates if set. // Reassign using the current frame and details.
if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength QuantizerOptions options = null;
&& frameMetadata.ColorTableLength > 0) int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
{ {
QuantizerOptions options = new() options = new()
{ {
Dither = this.quantizer.Options.Dither, Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale, 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.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(); // Do we have extension information to write?
quantized = null; // So next frame can regenerate it int index = GetTransparentIndex(quantized);
previousFrame = frame; if (metadata != null || index > -1)
previousMeta = frameMetadata; {
this.WriteGraphicalControlExtension(metadata ?? new(), index, stream);
} }
this.WriteImageDescriptor(frame, useLocal, stream);
if (useLocal)
{
this.WriteColorTable(quantized, stream);
}
this.WriteImageData(quantized, stream);
} }
/// <summary> /// <summary>
@ -407,7 +407,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
/// <summary> /// <summary>
/// Writes the graphics control extension to the stream. /// Writes the optional graphics control extension to the stream.
/// </summary> /// </summary>
/// <param name="metadata">The metadata of the image or frame.</param> /// <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> /// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>

8
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -21,13 +21,19 @@ public class GifFrameMetadata : IDeepCloneable
/// <param name="other">The metadata to create an instance from.</param> /// <param name="other">The metadata to create an instance from.</param>
private GifFrameMetadata(GifFrameMetadata other) private GifFrameMetadata(GifFrameMetadata other)
{ {
this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength; this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMethod = other.DisposalMethod;
} }
/// <summary> /// <summary>
/// Gets or sets the length of the color table for paletted images. /// Gets or sets the color table mode.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the length of the color table.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the /// If not 0, then this field indicates the maximum number of colors to use when quantizing the
/// image frame. /// image frame.
/// </summary> /// </summary>

2
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -50,7 +50,7 @@ public class GifMetadata : IDeepCloneable
public int GlobalColorTableLength { get; set; } public int GlobalColorTableLength { get; set; }
/// <summary> /// <summary>
/// Gets or sets the the collection of comments about the graphics, credits, descriptions or any /// Gets or sets the collection of comments about the graphics, credits, descriptions or any
/// other type of non-control and non-graphic data. /// other type of non-control and non-graphic data.
/// </summary> /// </summary>
public IList<string> Comments { get; set; } = new List<string>(); public IList<string> Comments { get; set; } = new List<string>();

9
src/ImageSharp/Formats/Gif/GifThrowHelper.cs

@ -1,12 +1,19 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
internal static class GifThrowHelper internal static class GifThrowHelper
{ {
[DoesNotReturn]
public static void ThrowInvalidImageContentException(string errorMessage) public static void ThrowInvalidImageContentException(string errorMessage)
=> throw new InvalidImageContentException(errorMessage); => throw new InvalidImageContentException(errorMessage);
public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException); [DoesNotReturn]
public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor.");
[DoesNotReturn]
public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data");
} }

22
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -14,14 +14,28 @@ public static partial class MetadataExtensions
/// <summary> /// <summary>
/// Gets the gif format specific metadata for the image. /// Gets the gif format specific metadata for the image.
/// </summary> /// </summary>
/// <param name="metadata">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns> /// <returns>The <see cref="GifMetadata"/>.</returns>
public static GifMetadata GetGifMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(GifFormat.Instance); public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
/// <summary> /// <summary>
/// Gets the gif format specific metadata for the image frame. /// Gets the gif format specific metadata for the image frame.
/// </summary> /// </summary>
/// <param name="metadata">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns> /// <returns>The <see cref="GifFrameMetadata"/>.</returns>
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(GifFormat.Instance); public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">
/// When this method returns, contains the metadata associated with the specified frame,
/// if found; otherwise, the default value for the type of the metadata parameter.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageFrameMetadata source, out GifFrameMetadata metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
} }

29
src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// processing a graphic rendering block. /// processing a graphic rendering block.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
internal readonly struct GifGraphicControlExtension : IGifExtension internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<GifGraphicControlExtension>
{ {
public GifGraphicControlExtension( public GifGraphicControlExtension(
byte packed, byte packed,
@ -64,6 +64,10 @@ internal readonly struct GifGraphicControlExtension : IGifExtension
int IGifExtension.ContentLength => 5; int IGifExtension.ContentLength => 5;
public static bool operator ==(GifGraphicControlExtension left, GifGraphicControlExtension right) => left.Equals(right);
public static bool operator !=(GifGraphicControlExtension left, GifGraphicControlExtension right) => !(left == right);
public int WriteTo(Span<byte> buffer) public int WriteTo(Span<byte> buffer)
{ {
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer)); ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));
@ -101,4 +105,27 @@ internal readonly struct GifGraphicControlExtension : IGifExtension
return value; return value;
} }
public override bool Equals(object obj) => obj is GifGraphicControlExtension extension && this.Equals(extension);
public bool Equals(GifGraphicControlExtension other)
=> this.BlockSize == other.BlockSize
&& this.Packed == other.Packed
&& this.DelayTime == other.DelayTime
&& this.TransparencyIndex == other.TransparencyIndex
&& this.DisposalMethod == other.DisposalMethod
&& this.TransparencyFlag == other.TransparencyFlag
&& ((IGifExtension)this).Label == ((IGifExtension)other).Label
&& ((IGifExtension)this).ContentLength == ((IGifExtension)other).ContentLength;
public override int GetHashCode()
=> HashCode.Combine(
this.BlockSize,
this.Packed,
this.DelayTime,
this.TransparencyIndex,
this.DisposalMethod,
this.TransparencyFlag,
((IGifExtension)this).Label,
((IGifExtension)this).ContentLength);
} }

1
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -28,7 +28,6 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="allocator">The memory allocator.</param> /// <param name="allocator">The memory allocator.</param>
/// <returns>The XMP metadata</returns> /// <returns>The XMP metadata</returns>
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
{ {
byte[] xmpBytes = ReadXmpData(stream, allocator); byte[] xmpBytes = ReadXmpData(stream, allocator);

8
src/ImageSharp/Formats/ImageDecoderUtilities.cs

@ -68,6 +68,10 @@ internal static class ImageDecoderUtilities
{ {
throw new InvalidImageContentException(decoder.Dimensions, ex); throw new InvalidImageContentException(decoder.Dimensions, ex);
} }
catch (Exception)
{
throw;
}
} }
internal static Image<TPixel> Decode<TPixel>( internal static Image<TPixel> Decode<TPixel>(
@ -96,6 +100,10 @@ internal static class ImageDecoderUtilities
{ {
throw largeImageExceptionFactory(ex, decoder.Dimensions); throw largeImageExceptionFactory(ex, decoder.Dimensions);
} }
catch (Exception)
{
throw;
}
} }
private static InvalidImageContentException DefaultLargeImageExceptionFactory( private static InvalidImageContentException DefaultLargeImageExceptionFactory(

31
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -69,7 +69,8 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
public ImageFrameMetadata DeepClone() => new(this); public ImageFrameMetadata DeepClone() => new(this);
/// <summary> /// <summary>
/// Gets the metadata value associated with the specified key. /// Gets the metadata value associated with the specified key. This method will always return a result creating
/// a new instance and binding it to the frame metadata if none is found.
/// </summary> /// </summary>
/// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam> /// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam>
/// <typeparam name="TFormatFrameMetadata">The type of format frame metadata.</typeparam> /// <typeparam name="TFormatFrameMetadata">The type of format frame metadata.</typeparam>
@ -90,4 +91,32 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
this.formatMetadata[key] = newMeta; this.formatMetadata[key] = newMeta;
return newMeta; return newMeta;
} }
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>
/// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam>
/// <typeparam name="TFormatFrameMetadata">The type of format frame metadata.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <param name="metadata">
/// When this method returns, contains the metadata associated with the specified key,
/// if the key is found; otherwise, the default value for the type of the metadata parameter.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the frame metadata exists for the specified key; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, out TFormatFrameMetadata metadata)
where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable
{
if (this.formatMetadata.TryGetValue(key, out IDeepCloneable meta))
{
metadata = (TFormatFrameMetadata)meta;
return true;
}
metadata = default;
return false;
}
} }

2
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs

@ -106,7 +106,7 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
// for higher bit depths. Lower bit depths will correctly reduce the palette. // for higher bit depths. Lower bit depths will correctly reduce the palette.
// TODO: Investigate more evenly reduced palette reduction. // TODO: Investigate more evenly reduced palette reduction.
int max = this.maxColors; int max = this.maxColors;
if (this.bitDepth == 8) if (this.bitDepth >= 4)
{ {
max--; max--;
} }

8
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -49,12 +49,8 @@ public class PaletteQuantizer : IQuantizer
{ {
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
// The palette quantizer can reuse the same pixel map across multiple frames // Always use the palette length over options since the palette cannot be reduced.
// since the palette is unchanging. This allows a reduction of memory usage across TPixel[] palette = new TPixel[this.colorPalette.Length];
// multi frame gifs using a global palette.
int length = Math.Min(this.colorPalette.Length, options.MaxColors);
TPixel[] palette = new TPixel[length];
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette); return new PaletteQuantizer<TPixel>(configuration, options, palette);
} }

38
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -201,4 +201,42 @@ public class GifEncoderTests
image.Dispose(); image.Dispose();
clone.Dispose(); clone.Dispose();
} }
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2288_A, PixelTypes.Rgba32)]
[WithFile(TestImages.Gif.Issues.Issue2288_B, PixelTypes.Rgba32)]
[WithFile(TestImages.Gif.Issues.Issue2288_C, PixelTypes.Rgba32)]
[WithFile(TestImages.Gif.Issues.Issue2288_D, PixelTypes.Rgba32)]
public void OptionalExtensionsShouldBeHandledProperly<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
int count = 0;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
{
count++;
}
}
provider.Utility.SaveTestOutputFile(image, extension: "gif");
using FileStream fs = File.OpenRead(provider.Utility.GetTestOutputFileName("gif"));
using Image<TPixel> image2 = Image.Load<TPixel>(fs);
Assert.Equal(image.Frames.Count, image2.Frames.Count);
count = 0;
foreach (ImageFrame<TPixel> frame in image2.Frames)
{
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
{
count++;
}
}
Assert.Equal(image2.Frames.Count, count);
}
} }

4
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs

@ -30,7 +30,9 @@ public class TiffEncoderMultiframeTests : TiffEncoderBaseTester
[Theory] [Theory]
[WithFile(TestImages.Gif.Receipt, PixelTypes.Rgb24)] [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgb24)]
[WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)]
// MAGICK decoder makes the same mistake we did and clones the proceeding frame overwriting the differences.
// [WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeMultiframe_Convert<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeMultiframe_Convert<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit48, TiffPhotometricInterpretation.Rgb); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit48, TiffPhotometricInterpretation.Rgb);

4
tests/ImageSharp.Tests/TestImages.cs

@ -476,6 +476,10 @@ public static class TestImages
public const string Issue1962NoColorTable = "Gif/issues/issue1962_tiniest_gif_1st.gif"; 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 Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif";
public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif"; public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif";
public const string Issue2288_A = "Gif/issues/issue_2288.gif";
public const string Issue2288_B = "Gif/issues/issue_2288_2.gif";
public const string Issue2288_C = "Gif/issues/issue_2288_3.gif";
public const string Issue2288_D = "Gif/issues/issue_2288_4.gif";
} }
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };

3
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs

@ -12,6 +12,7 @@ public class ExactImageComparer : ImageComparer
public static ExactImageComparer Instance { get; } = new ExactImageComparer(); public static ExactImageComparer Instance { get; } = new ExactImageComparer();
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>( public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
int index,
ImageFrame<TPixelA> expected, ImageFrame<TPixelA> expected,
ImageFrame<TPixelB> actual) ImageFrame<TPixelB> actual)
{ {
@ -52,6 +53,6 @@ public class ExactImageComparer : ImageComparer
} }
} }
return new ImageSimilarityReport<TPixelA, TPixelB>(expected, actual, differences); return new ImageSimilarityReport<TPixelA, TPixelB>(index, expected, actual, differences);
} }
} }

4
tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs

@ -33,13 +33,11 @@ public class ImageDifferenceIsOverThresholdException : ImagesSimilarityException
sb.AppendFormat("Test Environment is Mono : {0}", TestEnvironment.IsMono); sb.AppendFormat("Test Environment is Mono : {0}", TestEnvironment.IsMono);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
int i = 0;
foreach (ImageSimilarityReport r in reports) foreach (ImageSimilarityReport r in reports)
{ {
sb.Append("Report ImageFrame {i}: "); sb.AppendFormat("Report ImageFrame {0}: ", r.Index);
sb.Append(r); sb.Append(r);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
i++;
} }
return sb.ToString(); return sb.ToString();

7
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs

@ -27,6 +27,7 @@ public abstract class ImageComparer
=> Tolerant(imageThresholdInPercents / 100F, perPixelManhattanThreshold); => Tolerant(imageThresholdInPercents / 100F, perPixelManhattanThreshold);
public abstract ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>( public abstract ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
int index,
ImageFrame<TPixelA> expected, ImageFrame<TPixelA> expected,
ImageFrame<TPixelB> actual) ImageFrame<TPixelB> actual)
where TPixelA : unmanaged, IPixel<TPixelA> where TPixelA : unmanaged, IPixel<TPixelA>
@ -40,7 +41,7 @@ public static class ImageComparerExtensions
Image<TPixelA> expected, Image<TPixelA> expected,
Image<TPixelB> actual) Image<TPixelB> actual)
where TPixelA : unmanaged, IPixel<TPixelA> where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB> => comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame); where TPixelB : unmanaged, IPixel<TPixelB> => comparer.CompareImagesOrFrames(0, expected.Frames.RootFrame, actual.Frames.RootFrame);
public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>( public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>(
this ImageComparer comparer, this ImageComparer comparer,
@ -58,7 +59,7 @@ public static class ImageComparerExtensions
for (int i = 0; i < expected.Frames.Count; i++) for (int i = 0; i < expected.Frames.Count; i++)
{ {
ImageSimilarityReport<TPixelA, TPixelB> report = comparer.CompareImagesOrFrames(expected.Frames[i], actual.Frames[i]); ImageSimilarityReport<TPixelA, TPixelB> report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]);
if (!report.IsEmpty) if (!report.IsEmpty)
{ {
result.Add(report); result.Add(report);
@ -125,7 +126,7 @@ public static class ImageComparerExtensions
if (outsideChanges.Any()) if (outsideChanges.Any())
{ {
cleanedReports.Add(new ImageSimilarityReport<TPixelA, TPixelB>(r.ExpectedImage, r.ActualImage, outsideChanges, null)); cleanedReports.Add(new ImageSimilarityReport<TPixelA, TPixelB>(r.Index, r.ExpectedImage, r.ActualImage, outsideChanges, null));
} }
} }

9
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs

@ -9,17 +9,21 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
public class ImageSimilarityReport public class ImageSimilarityReport
{ {
protected ImageSimilarityReport( protected ImageSimilarityReport(
int index,
object expectedImage, object expectedImage,
object actualImage, object actualImage,
IEnumerable<PixelDifference> differences, IEnumerable<PixelDifference> differences,
float? totalNormalizedDifference = null) float? totalNormalizedDifference = null)
{ {
this.Index = index;
this.ExpectedImage = expectedImage; this.ExpectedImage = expectedImage;
this.ActualImage = actualImage; this.ActualImage = actualImage;
this.TotalNormalizedDifference = totalNormalizedDifference; this.TotalNormalizedDifference = totalNormalizedDifference;
this.Differences = differences.ToArray(); this.Differences = differences.ToArray();
} }
public int Index { get; }
public object ExpectedImage { get; } public object ExpectedImage { get; }
public object ActualImage { get; } public object ActualImage { get; }
@ -89,16 +93,17 @@ public class ImageSimilarityReport<TPixelA, TPixelB> : ImageSimilarityReport
where TPixelB : unmanaged, IPixel<TPixelB> where TPixelB : unmanaged, IPixel<TPixelB>
{ {
public ImageSimilarityReport( public ImageSimilarityReport(
int index,
ImageFrame<TPixelA> expectedImage, ImageFrame<TPixelA> expectedImage,
ImageFrame<TPixelB> actualImage, ImageFrame<TPixelB> actualImage,
IEnumerable<PixelDifference> differences, IEnumerable<PixelDifference> differences,
float? totalNormalizedDifference = null) float? totalNormalizedDifference = null)
: base(expectedImage, actualImage, differences, totalNormalizedDifference) : base(index, expectedImage, actualImage, differences, totalNormalizedDifference)
{ {
} }
public static ImageSimilarityReport<TPixelA, TPixelB> Empty => public static ImageSimilarityReport<TPixelA, TPixelB> Empty =>
new ImageSimilarityReport<TPixelA, TPixelB>(null, null, Enumerable.Empty<PixelDifference>(), 0f); new ImageSimilarityReport<TPixelA, TPixelB>(0, null, null, Enumerable.Empty<PixelDifference>(), 0f);
public new ImageFrame<TPixelA> ExpectedImage => (ImageFrame<TPixelA>)base.ExpectedImage; public new ImageFrame<TPixelA> ExpectedImage => (ImageFrame<TPixelA>)base.ExpectedImage;

4
tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs

@ -56,7 +56,7 @@ public class TolerantImageComparer : ImageComparer
/// </summary> /// </summary>
public int PerPixelManhattanThreshold { get; } public int PerPixelManhattanThreshold { get; }
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(ImageFrame<TPixelA> expected, ImageFrame<TPixelB> actual) public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(int index, ImageFrame<TPixelA> expected, ImageFrame<TPixelB> actual)
{ {
if (expected.Size() != actual.Size()) if (expected.Size() != actual.Size())
{ {
@ -103,7 +103,7 @@ public class TolerantImageComparer : ImageComparer
if (normalizedDifference > this.ImageThreshold) if (normalizedDifference > this.ImageThreshold)
{ {
return new ImageSimilarityReport<TPixelA, TPixelB>(expected, actual, differences, normalizedDifference); return new ImageSimilarityReport<TPixelA, TPixelB>(index, expected, actual, differences, normalizedDifference);
} }
else else
{ {

3
tests/Images/Input/Gif/issues/issue_2288.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d38bc98de425322bc0d435b7ff538c170897bfc728ea77ee26dd172106dcf99a
size 1223216

3
tests/Images/Input/Gif/issues/issue_2288_2.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8919e83c8a19502b3217c75e0a7c98be46732c2126816f8882e9bed19478ded7
size 811449

3
tests/Images/Input/Gif/issues/issue_2288_3.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3f0fd68a03e9c1c896e021828f470d9ceeb8f10e1aead230e42e55670520840
size 958995

3
tests/Images/Input/Gif/issues/issue_2288_4.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ac8bc8677a1eb4e26161e5255a5c651321ff063892f3caec3f95264aee38057
size 1971226
Loading…
Cancel
Save