Browse Source

Implement GifFrameMetadata

pull/2751/head
James Jackson-South 2 years ago
parent
commit
8166213c17
  1. 4
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  2. 4
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  3. 37
      src/ImageSharp/Formats/Gif/GifDisposalMethod.cs
  4. 12
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  5. 71
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  6. 4
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  7. 12
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  8. 6
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  9. 8
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  10. 5
      tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
  11. 4
      tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
  12. 9
      tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs
  13. 8
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  14. 9
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  15. 3
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

4
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -138,8 +138,8 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>, IFormatFrameMetadata<Bm
=> new();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => ((IDeepCloneable<BmpMetadata>)this).DeepClone();
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
BmpMetadata IDeepCloneable<BmpMetadata>.DeepClone() => new(this);
public BmpMetadata DeepClone() => new(this);
}

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

@ -517,7 +517,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
else
{
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
prevFrame = previousFrame;
}
@ -624,7 +624,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
previousFrame = currentFrame ?? image.Frames.RootFrame;
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground)
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
}

37
src/ImageSharp/Formats/Gif/GifDisposalMethod.cs

@ -1,37 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides enumeration for instructing the decoder what to do with the last image
/// in an animation sequence.
/// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23
/// </summary>
public enum GifDisposalMethod
{
/// <summary>
/// No disposal specified.
/// The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// Do not dispose.
/// The graphic is to be left in place.
/// </summary>
NotDispose = 1,
/// <summary>
/// Restore to background color.
/// The area used by the graphic must be restored to the background color.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// Restore to previous.
/// The decoder is required to restore the area overwritten by the
/// graphic with what was there prior to rendering the graphic.
/// </summary>
RestoreToPrevious = 3
}

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

@ -235,7 +235,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
GifDisposalMethod previousDisposalMethod)
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -279,10 +279,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMethod);
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMethod = gifMetadata.DisposalMethod;
previousDisposalMode = gifMetadata.DisposalMethod;
}
if (hasPaletteQuantizer)
@ -323,14 +323,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool useLocal,
GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
GifDisposalMethod previousDisposal)
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
@ -664,7 +664,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool hasTransparency = metadata.HasTransparency;
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod,
disposalMode: metadata.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(

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

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides Gif specific metadata information for the image frame.
/// </summary>
public class GifFrameMetadata : IDeepCloneable
public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="GifFrameMetadata"/> class.
@ -73,10 +73,65 @@ public class GifFrameMetadata : IDeepCloneable
/// Primarily used in Gif animation, this field indicates the way in which the graphic is to
/// be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod { get; set; }
public FrameDisposalMode DisposalMethod { get; set; }
/// <inheritdoc />
public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
int index = -1;
const float background = 1f;
if (metadata.ColorTable.HasValue)
{
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
Vector4 vector = colorTable[i].ToScaledVector4();
if (vector.W < background)
{
index = i;
}
}
}
bool hasTransparency = index >= 0;
return new()
{
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMethod = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
/// <inheritdoc />
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
{
// throw new NotImplementedException();
// For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or
// has a local palette with 256 colors and is not transparent we should use 'Source'.
bool blendSource = this.DisposalMethod == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency);
// If the color table is global and frame has no transparency. Consider it 'Source' also.
blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency;
return new()
{
ColorTable = this.LocalColorTable,
ColorTableMode = this.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10),
DisposalMode = this.DisposalMethod,
BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
public GifFrameMetadata DeepClone() => new(this);
internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
{
@ -103,17 +158,9 @@ public class GifFrameMetadata : IDeepCloneable
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMethod = GetMode(metadata.DisposalMode),
DisposalMethod = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
{
FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose,
FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious,
_ => GifDisposalMethod.Unspecified,
};
}

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

@ -145,8 +145,8 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => ((IDeepCloneable<GifMetadata>)this).DeepClone();
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
GifMetadata IDeepCloneable<GifMetadata>.DeepClone() => new(this);
public GifMetadata DeepClone() => new(this);
}

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

@ -80,7 +80,7 @@ public static partial class MetadataExtensions
{
// For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or
// has a local palette with 256 colors and is not transparent we should use 'Source'.
bool blendSource = source.DisposalMethod == GifDisposalMethod.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency);
bool blendSource = source.DisposalMethod == FrameDisposalMode.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency);
// If the color table is global and frame has no transparency. Consider it 'Source' also.
blendSource |= source.ColorTableMode == FrameColorTableMode.Global && !source.HasTransparency;
@ -90,16 +90,8 @@ public static partial class MetadataExtensions
ColorTable = source.LocalColorTable,
ColorTableMode = source.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10),
DisposalMode = GetMode(source.DisposalMethod),
DisposalMode = source.DisposalMethod,
BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch
{
GifDisposalMethod.NotDispose => FrameDisposalMode.DoNotDispose,
GifDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
GifDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
_ => FrameDisposalMode.Unspecified,
};
}

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

@ -52,7 +52,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
/// Gets the disposal method which indicates the way in which the
/// graphic is to be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2);
public FrameDisposalMode DisposalMethod => (FrameDisposalMode)((this.Packed & 0x1C) >> 2);
/// <summary>
/// Gets a value indicating whether transparency flag is to be set.
@ -80,7 +80,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
public static GifGraphicControlExtension Parse(ReadOnlySpan<byte> buffer)
=> MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false)
public static byte GetPackedValue(FrameDisposalMode disposalMode, bool userInputFlag = false, bool transparencyFlag = false)
{
/*
Reserved | 3 Bits
@ -91,7 +91,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
byte value = 0;
value |= (byte)((int)disposalMethod << 2);
value |= (byte)((int)disposalMode << 2);
if (userInputFlag)
{

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

@ -309,11 +309,11 @@ public class GifEncoderTests
switch (pngF.DisposalMethod)
{
case PngDisposalMethod.RestoreToBackground:
Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod);
Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod);
break;
case PngDisposalMethod.DoNotDispose:
default:
Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod);
Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod);
break;
}
}
@ -359,11 +359,11 @@ public class GifEncoderTests
switch (webpF.DisposalMethod)
{
case WebpDisposalMethod.RestoreToBackground:
Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod);
Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMethod);
break;
case WebpDisposalMethod.DoNotDispose:
default:
Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod);
Assert.Equal(FrameDisposalMode.DoNotDispose, gifF.DisposalMethod);
break;
}
}

5
tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
namespace SixLabors.ImageSharp.Tests.Formats.Gif;
@ -14,14 +15,14 @@ public class GifFrameMetadataTests
GifFrameMetadata meta = new()
{
FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground,
DisposalMethod = FrameDisposalMode.RestoreToBackground,
LocalColorTable = new[] { Color.Black, Color.White }
};
GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone();
clone.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.DisposalMethod = FrameDisposalMode.RestoreToPrevious;
clone.LocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));

4
tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs

@ -183,14 +183,14 @@ public class GifMetadataTests
}
[Theory]
[InlineData(TestImages.Gif.Cheers, 93, FrameColorTableMode.Global, 256, 4, GifDisposalMethod.NotDispose)]
[InlineData(TestImages.Gif.Cheers, 93, FrameColorTableMode.Global, 256, 4, FrameDisposalMode.DoNotDispose)]
public void Identify_Frames(
string imagePath,
int framesCount,
FrameColorTableMode colorTableMode,
int globalColorTableLength,
int frameDelay,
GifDisposalMethod disposalMethod)
FrameDisposalMode disposalMethod)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);

9
tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
namespace SixLabors.ImageSharp.Tests.Formats.Gif.Sections;
@ -10,9 +11,9 @@ public class GifGraphicControlExtensionTests
[Fact]
public void TestPackedValue()
{
Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.Unspecified, false, false));
Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToBackground, true, true));
Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.NotDispose, false, false));
Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToPrevious, true, false));
Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.Unspecified, false, false));
Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.RestoreToBackground, true, true));
Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.DoNotDispose, false, false));
Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(FrameDisposalMode.RestoreToPrevious, true, false));
}
}

8
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -521,14 +521,14 @@ public partial class PngEncoderTests
switch (gifF.DisposalMethod)
{
case GifDisposalMethod.RestoreToBackground:
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod);
break;
case GifDisposalMethod.RestoreToPrevious:
case FrameDisposalMode.RestoreToPrevious:
Assert.Equal(PngDisposalMethod.RestoreToPrevious, pngF.DisposalMethod);
break;
case GifDisposalMethod.Unspecified:
case GifDisposalMethod.NotDispose:
case FrameDisposalMode.Unspecified:
case FrameDisposalMode.DoNotDispose:
default:
Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod);
break;

9
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
@ -96,12 +97,12 @@ public class WebpEncoderTests
switch (gifF.DisposalMethod)
{
case GifDisposalMethod.RestoreToBackground:
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod);
break;
case GifDisposalMethod.RestoreToPrevious:
case GifDisposalMethod.Unspecified:
case GifDisposalMethod.NotDispose:
case FrameDisposalMode.RestoreToPrevious:
case FrameDisposalMode.Unspecified:
case FrameDisposalMode.DoNotDispose:
default:
Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod);
break;

3
tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -21,7 +22,7 @@ public class ImageFrameMetadataTests
{
const int frameDelay = 42;
const int colorTableLength = 128;
const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground;
const FrameDisposalMode disposalMethod = FrameDisposalMode.RestoreToBackground;
ImageFrameMetadata metaData = new();
GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata();

Loading…
Cancel
Save