Browse Source

Merge pull request #2289 from SixLabors/defect/2288

Fix various Gif Decoder/Encoder behaviors.
pull/2237/merge
James Jackson-South 3 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);
// Reset per-frame state.
this.imageDescriptor = default;
this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
@ -161,6 +165,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.globalColorTable?.Dispose();
}
if (image is null)
{
GifThrowHelper.ThrowNoData();
}
return image;
}
@ -214,6 +223,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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,
@ -277,40 +291,44 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// 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;
}
@ -464,7 +482,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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;
}
@ -475,9 +493,9 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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;
@ -554,13 +572,18 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
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);
}
}
}
@ -592,7 +615,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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();
@ -603,28 +626,34 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// 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>
@ -639,7 +668,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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

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

@ -93,7 +93,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Quantize the image returning a palette.
IndexedImageFrame<TPixel> quantized;
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
if (useGlobalTable)
@ -133,101 +132,102 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
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>
@ -407,7 +407,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
/// <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>

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>
private GifFrameMetadata(GifFrameMetadata other)
{
this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
}
/// <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
/// image frame.
/// </summary>

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

@ -50,7 +50,7 @@ public class GifMetadata : IDeepCloneable
public int GlobalColorTableLength { get; set; }
/// <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.
/// </summary>
public IList<string> Comments { get; set; } = new List<string>();

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

@ -1,12 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Gif;
internal static class GifThrowHelper
{
[DoesNotReturn]
public static void ThrowInvalidImageContentException(string 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>
/// Gets the gif format specific metadata for the image.
/// </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>
public static GifMetadata GetGifMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(GifFormat.Instance);
public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
/// </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>
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.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal readonly struct GifGraphicControlExtension : IGifExtension
internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<GifGraphicControlExtension>
{
public GifGraphicControlExtension(
byte packed,
@ -64,6 +64,10 @@ internal readonly struct GifGraphicControlExtension : IGifExtension
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)
{
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));
@ -101,4 +105,27 @@ internal readonly struct GifGraphicControlExtension : IGifExtension
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="allocator">The memory allocator.</param>
/// <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)
{
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);
}
catch (Exception)
{
throw;
}
}
internal static Image<TPixel> Decode<TPixel>(
@ -96,6 +100,10 @@ internal static class ImageDecoderUtilities
{
throw largeImageExceptionFactory(ex, decoder.Dimensions);
}
catch (Exception)
{
throw;
}
}
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);
/// <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>
/// <typeparam name="TFormatMetadata">The type of format 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;
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.
// TODO: Investigate more evenly reduced palette reduction.
int max = this.maxColors;
if (this.bitDepth == 8)
if (this.bitDepth >= 4)
{
max--;
}

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

@ -49,12 +49,8 @@ public class PaletteQuantizer : IQuantizer
{
Guard.NotNull(options, nameof(options));
// 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.
int length = Math.Min(this.colorPalette.Length, options.MaxColors);
TPixel[] palette = new TPixel[length];
// Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette);
}

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

@ -201,4 +201,42 @@ public class GifEncoderTests
image.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]
[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)
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 Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.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 };

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

@ -12,6 +12,7 @@ public class ExactImageComparer : ImageComparer
public static ExactImageComparer Instance { get; } = new ExactImageComparer();
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
int index,
ImageFrame<TPixelA> expected,
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.Append(Environment.NewLine);
int i = 0;
foreach (ImageSimilarityReport r in reports)
{
sb.Append("Report ImageFrame {i}: ");
sb.AppendFormat("Report ImageFrame {0}: ", r.Index);
sb.Append(r);
sb.Append(Environment.NewLine);
i++;
}
return sb.ToString();

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

@ -27,6 +27,7 @@ public abstract class ImageComparer
=> Tolerant(imageThresholdInPercents / 100F, perPixelManhattanThreshold);
public abstract ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
int index,
ImageFrame<TPixelA> expected,
ImageFrame<TPixelB> actual)
where TPixelA : unmanaged, IPixel<TPixelA>
@ -40,7 +41,7 @@ public static class ImageComparerExtensions
Image<TPixelA> expected,
Image<TPixelB> actual)
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>(
this ImageComparer comparer,
@ -58,7 +59,7 @@ public static class ImageComparerExtensions
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)
{
result.Add(report);
@ -125,7 +126,7 @@ public static class ImageComparerExtensions
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
{
protected ImageSimilarityReport(
int index,
object expectedImage,
object actualImage,
IEnumerable<PixelDifference> differences,
float? totalNormalizedDifference = null)
{
this.Index = index;
this.ExpectedImage = expectedImage;
this.ActualImage = actualImage;
this.TotalNormalizedDifference = totalNormalizedDifference;
this.Differences = differences.ToArray();
}
public int Index { get; }
public object ExpectedImage { get; }
public object ActualImage { get; }
@ -89,16 +93,17 @@ public class ImageSimilarityReport<TPixelA, TPixelB> : ImageSimilarityReport
where TPixelB : unmanaged, IPixel<TPixelB>
{
public ImageSimilarityReport(
int index,
ImageFrame<TPixelA> expectedImage,
ImageFrame<TPixelB> actualImage,
IEnumerable<PixelDifference> differences,
float? totalNormalizedDifference = null)
: base(expectedImage, actualImage, differences, totalNormalizedDifference)
: base(index, expectedImage, actualImage, differences, totalNormalizedDifference)
{
}
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;

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

@ -56,7 +56,7 @@ public class TolerantImageComparer : ImageComparer
/// </summary>
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())
{
@ -103,7 +103,7 @@ public class TolerantImageComparer : ImageComparer
if (normalizedDifference > this.ImageThreshold)
{
return new ImageSimilarityReport<TPixelA, TPixelB>(expected, actual, differences, normalizedDifference);
return new ImageSimilarityReport<TPixelA, TPixelB>(index, expected, actual, differences, normalizedDifference);
}
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