Browse Source

Preserve color palettes and deduplicate frame pixels.

pull/2500/head
James Jackson-South 3 years ago
parent
commit
12da625cbb
  1. 27
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  2. 6
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  3. 3
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 67
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  5. 229
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  6. 27
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  7. 17
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  8. 9
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  9. 35
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  10. 9
      src/ImageSharp/Formats/Png/PngEncoder.cs
  11. 3
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  12. 6
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  13. 5
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  14. 11
      src/ImageSharp/Memory/Buffer2D{T}.cs
  15. 8
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  16. 39
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  17. 11
      tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
  18. 15
      tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
  19. 9
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  20. 19
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

27
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -629,6 +629,33 @@ internal static partial class SimdUtils
return Avx.Subtract(c, Avx.Multiply(a, b));
}
/// <summary>
/// Blend packed 8-bit integers from <paramref name="left"/> and <paramref name="right"/> using <paramref name="mask"/>.
/// The high bit of each corresponding <paramref name="mask"/> byte determines the selection.
/// If the high bit is set the element of <paramref name="left"/> is selected.
/// The element of <paramref name="right"/> is selected otherwise.
/// </summary>
/// <param name="left">The left vector.</param>
/// <param name="right">The right vector.</param>
/// <param name="mask">The mask vector.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> BlendVariable(in Vector128<byte> left, in Vector128<byte> right, in Vector128<byte> mask)
{
if (Sse41.IsSupported)
{
return Sse41.BlendVariable(left, right, mask);
}
else if (Sse2.IsSupported)
{
return Sse2.Or(Sse2.And(right, mask), Sse2.AndNot(mask, left));
}
// Use a signed shift right to create a mask with the sign bit.
Vector128<short> signedMask = AdvSimd.ShiftRightArithmetic(mask.AsInt16(), 7);
return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte();
}
/// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary>

6
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Bmp;
@ -10,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp;
/// </summary>
public sealed class BmpEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
/// </summary>
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>

3
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
@ -100,7 +101,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
}

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

@ -29,6 +29,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// </summary>
private IMemoryOwner<byte>? globalColorTable;
/// <summary>
/// The current local color table.
/// </summary>
private IMemoryOwner<byte>? currentLocalColorTable;
/// <summary>
/// Gets the size in bytes of the current local color table.
/// </summary>
private int currentLocalColorTableSize;
/// <summary>
/// The area to restore.
/// </summary>
@ -159,6 +169,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (image is null)
@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount;
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
stream.Skip(1); // Skip the terminator.
return;
}
@ -415,25 +427,27 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
this.ReadImageDescriptor(stream);
IMemoryOwner<byte>? localColorTable = null;
Buffer2D<byte>? indices = null;
try
{
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
if (this.imageDescriptor.LocalColorTableFlag)
bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
if (hasLocalColorTable)
{
int length = this.imageDescriptor.LocalColorTableSize * 3;
localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean);
stream.Read(localColorTable.GetSpan());
// Read and store the local color table. We allocate the maximum possible size and slice to match.
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default;
if (localColorTable != null)
if (hasLocalColorTable)
{
rawColorTable = localColorTable.GetSpan();
rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
}
else if (this.globalColorTable != null)
{
@ -448,7 +462,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
finally
{
localColorTable?.Dispose();
indices?.Dispose();
}
}
@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame;
}
currentFrame = image!.Frames.CreateFrame();
// We create a clone of the frame and add it.
// We will overpaint the difference of pixels on the current frame to create a complete image.
// This ensures that we have enough pixel data to process without distortion. #2450
currentFrame = image!.Frames.AddFrame(previousFrame);
this.SetFrameMetadata(currentFrame.Metadata);
@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// Skip the color table for this frame if local.
if (this.imageDescriptor.LocalColorTableFlag)
{
stream.Skip(this.imageDescriptor.LocalColorTableSize * 3);
// Read and store the local color table. We allocate the maximum possible size and slice to match.
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
// Skip the frame indices. Pixels length + mincode size.
@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
if (this.imageDescriptor.LocalColorTableFlag
@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
ref Rgb24 localBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]));
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(Unsafe.Add(ref localBase, (uint)i));
}
gifMeta.DecodedLocalColorTable = colorTable;
}
// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}
@ -751,14 +779,21 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
if (globalColorTableLength > 0)
{
this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean);
// Read the global color table data from the stream
// Read the global color table data from the stream and preserve it in the gif metadata
stream.Read(this.globalColorTable.GetSpan());
Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
ref Rgb24 globalBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(this.globalColorTable.GetSpan()));
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(Unsafe.Add(ref globalBase, (uint)i));
}
this.gifMetadata.DecodedGlobalColorTable = colorTable;
}
}
}

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

@ -4,11 +4,15 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -36,7 +40,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private readonly IQuantizer quantizer;
private IQuantizer? quantizer;
/// <summary>
/// Whether the quantizer was supplied via options.
/// </summary>
private readonly bool hasQuantizer;
/// <summary>
/// The color table mode: Global or local.
@ -64,6 +73,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.memoryAllocator = configuration.MemoryAllocator;
this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
}
@ -88,6 +98,21 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Quantize the image returning a palette.
IndexedImageFrame<TPixel>? quantized;
if (this.quantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.DecodedGlobalColorTable.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
this.quantizer = new PaletteQuantizer(gifMetadata.DecodedGlobalColorTable, new() { Dither = null });
}
else
{
this.quantizer = KnownQuantizers.Octree;
}
}
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
if (useGlobalTable)
@ -109,7 +134,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
WriteHeader(stream);
// Write the LSD.
int index = GetTransparentIndex(quantized);
image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
int index = GetTransparentIndex(quantized, frameMetadata);
if (index == -1)
{
index = gifMetadata.BackgroundColor;
}
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream);
if (useGlobalTable)
@ -141,6 +172,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{
PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false;
// Create a buffer to store de-duplicated pixel indices for encoding.
// These are used when the color table is global but we must always allocate since we don't know
// in advance whether the frames will use a local palette.
Buffer2D<byte> indices = this.memoryAllocator.Allocate2D<byte>(image.Width, image.Height);
// Store the first frame as a reference for de-duplication comparison.
IndexedImageFrame<TPixel> previousQuantized = quantized;
for (int i = 0; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
@ -155,15 +194,21 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// 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);
paletteQuantizer = new(this.configuration, this.quantizer!.Options, palette);
}
this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer);
this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, indices, ref previousQuantized, ref quantized!, ref paletteQuantizer);
// Clean up for the next run.
quantized.Dispose();
if (quantized != previousQuantized)
{
quantized.Dispose();
}
}
previousQuantized.Dispose();
indices.Dispose();
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
@ -176,47 +221,55 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
int frameIndex,
bool useLocal,
GifFrameMetadata? metadata,
Buffer2D<byte> indices,
ref IndexedImageFrame<TPixel> previousQuantized,
ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> paletteQuantizer)
ref PaletteQuantizer<TPixel> globalPaletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel>
{
// The first frame has already been quantized so we do not need to do so again.
int transparencyIndex = -1;
if (frameIndex > 0)
{
if (useLocal)
{
// Reassign using the current frame and details.
QuantizerOptions? options = null;
int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
if (metadata?.DecodedLocalColorTable.Length > 0)
{
options = new()
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = colorTableLength
};
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
PaletteQuantizer localQuantizer = new(metadata.DecodedLocalColorTable, new() { Dither = null });
using IQuantizer<TPixel> frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, localQuantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}
else
{
// We must quantize the frame to generate a local color table.
IQuantizer localQuantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
using IQuantizer<TPixel> frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, localQuantizer.Options);
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());
quantized = globalPaletteQuantizer.QuantizeFrame(frame, frame.Bounds());
transparencyIndex = GetTransparentIndex(quantized, metadata);
// De-duplicate pixels comparing to the previous frame.
// Only global is supported for now as the color palettes as the operation required to compare
// and offset the index lookups is too expensive for local palettes.
DeDuplicatePixels(previousQuantized, quantized, indices, transparencyIndex);
}
this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
}
// Do we have extension information to write?
int index = GetTransparentIndex(quantized);
if (metadata != null || index > -1)
else
{
this.WriteGraphicalControlExtension(metadata ?? new(), index, stream);
transparencyIndex = GetTransparentIndex(quantized, metadata);
}
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
this.WriteImageDescriptor(frame, useLocal, stream);
if (useLocal)
@ -224,18 +277,103 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteColorTable(quantized, stream);
}
this.WriteImageData(quantized, stream);
// Assign the correct buffer to compress.
// If we are using a local palette or it's the first run then we want to use the quantized frame.
Buffer2D<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
this.WriteImageData(buffer, stream);
// Swap the buffers.
(quantized, previousQuantized) = (previousQuantized, quantized);
}
private static void DeDuplicatePixels<TPixel>(
IndexedImageFrame<TPixel> background,
IndexedImageFrame<TPixel> source,
Buffer2D<byte> indices,
int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: This should be the background color if not transparent.
byte replacementIndex = unchecked((byte)transparencyIndex);
for (int y = 0; y < background.Height; y++)
{
ref byte backgroundRowBase = ref MemoryMarshal.GetReference(background.DangerousGetRowSpan(y));
ref byte sourceRowBase = ref MemoryMarshal.GetReference(source.DangerousGetRowSpan(y));
ref byte indicesRowBase = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y));
uint x = 0;
if (Avx2.IsSupported)
{
int remaining = background.Width;
Vector256<byte> transparentVector = Vector256.Create(replacementIndex);
while (remaining >= Vector256<byte>.Count)
{
Vector256<byte> b = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref backgroundRowBase, x));
Vector256<byte> s = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref sourceRowBase, x));
Vector256<byte> m = Avx2.CompareEqual(b, s);
Vector256<byte> i = Avx2.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
x += (uint)Vector256<byte>.Count;
remaining -= Vector256<byte>.Count;
}
}
else if (Sse2.IsSupported)
{
int remaining = background.Width;
Vector128<byte> transparentVector = Vector128.Create(replacementIndex);
while (remaining >= Vector128<byte>.Count)
{
Vector128<byte> b = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref backgroundRowBase, x));
Vector128<byte> s = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref sourceRowBase, x));
Vector128<byte> m = Sse2.CompareEqual(b, s);
Vector128<byte> i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
x += (uint)Vector128<byte>.Count;
remaining -= Vector128<byte>.Count;
}
}
else if (AdvSimd.Arm64.IsSupported)
{
int remaining = background.Width;
Vector128<byte> transparentVector = Vector128.Create(replacementIndex);
while (remaining >= Vector128<byte>.Count)
{
Vector128<byte> b = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref backgroundRowBase, x));
Vector128<byte> s = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref sourceRowBase, x));
Vector128<byte> m = AdvSimd.CompareEqual(b, s);
Vector128<byte> i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
x += (uint)Vector128<byte>.Count;
remaining -= Vector128<byte>.Count;
}
}
for (; x < (uint)background.Width; x++)
{
byte b = Unsafe.Add(ref backgroundRowBase, x);
byte s = Unsafe.Add(ref sourceRowBase, x);
ref byte i = ref Unsafe.Add(ref indicesRowBase, x);
i = (b == s) ? replacementIndex : s;
}
}
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">The quantized frame.</param>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette.
@ -255,6 +393,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
if (metadata?.HasTransparency == true && index == -1)
{
index = metadata.TransparencyIndex;
}
return index;
}
@ -271,14 +414,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The image metadata.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
/// <param name="backgroundIndex">The index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor(
ImageMetadata metadata,
int width,
int height,
int transparencyIndex,
int backgroundIndex,
bool useGlobalTable,
Stream stream)
{
@ -316,7 +459,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
width: (ushort)width,
height: (ushort)height,
packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex),
backgroundColorIndex: unchecked((byte)backgroundIndex),
ratio);
Span<byte> buffer = stackalloc byte[20];
@ -412,16 +555,26 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <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="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int transparencyIndex, Stream stream)
private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
{
bool hasTransparency;
if (metadata is null)
{
hasTransparency = transparencyIndex > -1;
}
else
{
hasTransparency = metadata.HasTransparency;
}
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod,
transparencyFlag: transparencyIndex > -1);
disposalMethod: metadata!.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
delayTime: (ushort)metadata.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex));
transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream);
}
@ -521,13 +674,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="indices">The <see cref="Buffer2D{Byte}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
private void WriteImageData(Buffer2D<byte> indices, Stream stream)
{
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(((IPixelSource)image).PixelBuffer, stream);
encoder.Encode(indices, stream);
}
}

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

@ -22,9 +22,16 @@ public class GifFrameMetadata : IDeepCloneable
private GifFrameMetadata(GifFrameMetadata other)
{
this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
if (other.DecodedLocalColorTable.Length > 0)
{
this.DecodedLocalColorTable = other.DecodedLocalColorTable.ToArray();
}
this.HasTransparency = other.HasTransparency;
this.TransparencyIndex = other.TransparencyIndex;
}
/// <summary>
@ -33,11 +40,21 @@ public class GifFrameMetadata : IDeepCloneable
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.
/// Gets the decoded global color table, if any.
/// </summary>
public ReadOnlyMemory<Color> DecodedLocalColorTable { get; internal set; }
/// <summary>
/// Gets or sets a value indicating whether the frame has transparency
/// </summary>
public bool HasTransparency { get; set; }
/// <summary>
/// Gets or sets the transparency index.
/// When <see cref="HasTransparency"/> is set to <see langword="true"/> this value indicates the index within
/// the color palette at which the transparent color is located.
/// </summary>
public int ColorTableLength { get; set; }
public byte TransparencyIndex { get; set; }
/// <summary>
/// Gets or sets the frame delay for animated images.

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

@ -23,7 +23,12 @@ public class GifMetadata : IDeepCloneable
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.GlobalColorTableLength = other.GlobalColorTableLength;
this.BackgroundColor = other.BackgroundColor;
if (other.DecodedGlobalColorTable.Length > 0)
{
this.DecodedGlobalColorTable = other.DecodedGlobalColorTable.ToArray();
}
for (int i = 0; i < other.Comments.Count; i++)
{
@ -45,9 +50,15 @@ public class GifMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the length of the global color table if present.
/// Gets the decoded global color table, if any.
/// </summary>
public ReadOnlyMemory<Color> DecodedGlobalColorTable { get; internal set; }
/// <summary>
/// Gets or sets the index at the <see cref="DecodedGlobalColorTable"/> for the background color.
/// The background color is the color used for those pixels on the screen that are not covered by an image.
/// </summary>
public int GlobalColorTableLength { get; set; }
public byte BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any

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

@ -17,14 +17,16 @@ public static partial class MetadataExtensions
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns>
public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.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="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns>
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.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.
@ -38,5 +40,6 @@ public static partial class MetadataExtensions
/// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
}

35
src/ImageSharp/Formats/Png/Filters/PaethFilter.cs

@ -35,9 +35,9 @@ internal static class PaethFilter
// row: a d
// The Paeth function predicts d to be whichever of a, b, or c is nearest to
// p = a + b - c.
if (Sse41.IsSupported && bytesPerPixel is 4)
if (Sse2.IsSupported && bytesPerPixel is 4)
{
DecodeSse41(scanline, previousScanline);
DecodeSse3(scanline, previousScanline);
}
else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{
@ -50,7 +50,7 @@ internal static class PaethFilter
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeSse41(Span<byte> scanline, Span<byte> previousScanline)
private static void DecodeSse3(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@ -90,8 +90,8 @@ internal static class PaethFilter
Vector128<short> smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
// Note `_epi8`: we need addition to wrap modulo 255.
d = Sse2.Add(d, nearest);
@ -143,8 +143,8 @@ internal static class PaethFilter
Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest);
@ -157,27 +157,6 @@ internal static class PaethFilter
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> BlendVariable(Vector128<byte> a, Vector128<byte> b, Vector128<byte> c)
{
// Equivalent of Sse41.BlendVariable:
// Blend packed 8-bit integers from a and b using mask, and store the results in
// dst.
//
// FOR j := 0 to 15
// i := j*8
// IF mask[i+7]
// dst[i+7:i] := b[i+7:i]
// ELSE
// dst[i+7:i] := a[i+7:i]
// FI
// ENDFOR
//
// Use a signed shift right to create a mask with the sign bit.
Vector128<short> mask = AdvSimd.ShiftRightArithmetic(c.AsInt16(), 7);
return AdvSimd.BitwiseSelect(mask, b.AsInt16(), a.AsInt16()).AsByte();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, uint bytesPerPixel)
{

9
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -11,15 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary>
public class PngEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder() =>
// We set the quantizer to null here to allow the underlying encoder to create a
// quantizer with options appropriate to the encoding bit depth.
this.Quantizer = null;
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType" /> values.

3
src/ImageSharp/Formats/QuantizingImageEncoder.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree;
public IQuantizer? Quantizer { get; init; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.

6
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -12,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// </summary>
public class TiffEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoder"/> class.
/// </summary>
public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>

5
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -85,7 +86,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
@ -320,7 +321,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{
int sz = ExifWriter.WriteValue(entry, buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(buffer.Slice(0, sz));
writer.WritePadded(buffer[..sz]);
}
else
{

11
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -173,13 +173,15 @@ public sealed class Buffer2D<T> : IDisposable
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
/// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
/// </summary>
/// <param name="destination">The destination buffer.</param>
/// <param name="source">The source buffer.</param>
/// <exception cref="InvalidMemoryOperationException">Attempt to copy/swap incompatible buffers.</exception>
internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{
bool swapped = false;
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{
(destination.FastMemoryGroup, source.FastMemoryGroup) =
(source.FastMemoryGroup, destination.FastMemoryGroup);
(destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true;
@ -201,7 +203,6 @@ public sealed class Buffer2D<T> : IDisposable
}
[MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y) =>
throw new ArgumentOutOfRangeException(
$"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
private void ThrowYOutOfRangeException(int y)
=> throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
}

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

@ -25,8 +25,8 @@ public class QuantizerOptions
/// </summary>
public float DitherScale
{
get { return this.ditherScale; }
set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); }
get => this.ditherScale;
set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
}
/// <summary>
@ -35,7 +35,7 @@ public class QuantizerOptions
/// </summary>
public int MaxColors
{
get { return this.maxColors; }
set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); }
get => this.maxColors;
set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
}

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

@ -171,10 +171,21 @@ public class GifEncoderTests
GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode;
int maxColors;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.DecodedGlobalColorTable.Length;
}
else
{
maxColors = frameMetadata.DecodedLocalColorTable.Length;
}
GifEncoder encoder = new()
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
};
image.Save(outStream, encoder);
@ -187,15 +198,31 @@ public class GifEncoderTests
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetadata.ColorTableLength);
colorMode = cloneMetadata.ColorTableMode;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.DecodedGlobalColorTable.Length;
}
else
{
maxColors = frameMetadata.DecodedLocalColorTable.Length;
}
Assert.Equal(64, maxColors);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cMeta = clone.Frames[i].Metadata.GetGifMetadata();
if (iMeta.ColorTableMode == GifColorTableMode.Local)
{
Assert.Equal(iMeta.DecodedLocalColorTable.Length, cMeta.DecodedLocalColorTable.Length);
}
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
Assert.Equal(iMeta.FrameDelay, cMeta.FrameDelay);
Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
}
image.Dispose();

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

@ -11,21 +11,22 @@ public class GifFrameMetadataTests
[Fact]
public void CloneIsDeep()
{
var meta = new GifFrameMetadata
GifFrameMetadata meta = new()
{
FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground,
ColorTableLength = 2
DecodedLocalColorTable = new[] { Color.Black, Color.White }
};
var clone = (GifFrameMetadata)meta.DeepClone();
GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone();
clone.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.ColorTableLength = 1;
clone.DecodedLocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength));
Assert.False(meta.DecodedLocalColorTable.Length == clone.DecodedLocalColorTable.Length);
Assert.Equal(1, clone.DecodedLocalColorTable.Length);
}
}

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using Microsoft.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
@ -35,7 +34,7 @@ public class GifMetadataTests
{
RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2,
DecodedGlobalColorTable = new[] { Color.Black, Color.White },
Comments = new List<string> { "Foo" }
};
@ -43,11 +42,12 @@ public class GifMetadataTests
clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local;
clone.GlobalColorTableLength = 1;
clone.DecodedGlobalColorTable = new[] { Color.Black };
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode));
Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength));
Assert.False(meta.DecodedGlobalColorTable.Length == clone.DecodedGlobalColorTable.Length);
Assert.Equal(1, clone.DecodedGlobalColorTable.Length);
Assert.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(clone.Comments));
}
@ -205,7 +205,12 @@ public class GifMetadataTests
GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata();
Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode);
Assert.Equal(globalColorTableLength, gifFrameMetadata.ColorTableLength);
if (colorTableMode == GifColorTableMode.Global)
{
Assert.Equal(globalColorTableLength, gifMetadata.DecodedGlobalColorTable.Length);
}
Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay);
Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod);
}

9
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs

@ -279,6 +279,7 @@ public abstract partial class ImageFrameCollectionTests
{
using Image source = provider.GetImage();
using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height);
// Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
@ -289,7 +290,7 @@ public abstract partial class ImageFrameCollectionTests
// Drop the original empty root frame:
dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif");
dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false);
dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++)
@ -314,7 +315,11 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(aData.DisposalMethod, bData.DisposalMethod);
Assert.Equal(aData.FrameDelay, bData.FrameDelay);
Assert.Equal(aData.ColorTableLength, bData.ColorTableLength);
if (aData.ColorTableMode == GifColorTableMode.Local && bData.ColorTableMode == GifColorTableMode.Local)
{
Assert.Equal(aData.DecodedLocalColorTable.Length, bData.DecodedLocalColorTable.Length);
}
}
}
}

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

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -22,17 +23,17 @@ public class ImageFrameMetadataTests
const int colorTableLength = 128;
const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground;
var metaData = new ImageFrameMetadata();
ImageFrameMetadata metaData = new();
GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata();
gifFrameMetadata.FrameDelay = frameDelay;
gifFrameMetadata.ColorTableLength = colorTableLength;
gifFrameMetadata.DecodedLocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray();
gifFrameMetadata.DisposalMethod = disposalMethod;
var clone = new ImageFrameMetadata(metaData);
ImageFrameMetadata clone = new(metaData);
GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata();
Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.DecodedLocalColorTable.Length);
Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod);
}
@ -40,19 +41,19 @@ public class ImageFrameMetadataTests
public void CloneIsDeep()
{
// arrange
var exifProfile = new ExifProfile();
ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]);
var iccProfile = new IccProfile()
XmpProfile xmpProfile = new(Array.Empty<byte>());
IccProfile iccProfile = new()
{
Header = new IccProfileHeader()
{
CmmType = "Unittest"
}
};
var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile();
var metaData = new ImageFrameMetadata()
IptcProfile iptcProfile = new();
ImageFrameMetadata metaData = new()
{
XmpProfile = xmpProfile,
ExifProfile = exifProfile,

Loading…
Cancel
Save