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)); 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> /// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder). /// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary> /// </summary>

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

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

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

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;
@ -100,7 +101,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel; this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer; this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; 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> /// </summary>
private IMemoryOwner<byte>? globalColorTable; 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> /// <summary>
/// The area to restore. /// The area to restore.
/// </summary> /// </summary>
@ -159,6 +169,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally finally
{ {
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
} }
if (image is null) if (image is null)
@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally finally
{ {
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
} }
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
stream.Read(this.buffer.Span, 0, 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. stream.Skip(1); // Skip the terminator.
return; return;
} }
@ -415,25 +427,27 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
this.ReadImageDescriptor(stream); this.ReadImageDescriptor(stream);
IMemoryOwner<byte>? localColorTable = null;
Buffer2D<byte>? indices = null; Buffer2D<byte>? indices = null;
try try
{ {
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. // 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; // Read and store the local color table. We allocate the maximum possible size and slice to match.
localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean); int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
stream.Read(localColorTable.GetSpan()); 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); indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices); this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default; Span<byte> rawColorTable = default;
if (localColorTable != null) if (hasLocalColorTable)
{ {
rawColorTable = localColorTable.GetSpan(); rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
} }
else if (this.globalColorTable != null) else if (this.globalColorTable != null)
{ {
@ -448,7 +462,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
finally finally
{ {
localColorTable?.Dispose();
indices?.Dispose(); indices?.Dispose();
} }
} }
@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame; 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); this.SetFrameMetadata(currentFrame.Metadata);
@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// Skip the color table for this frame if local. // Skip the color table for this frame if local.
if (this.imageDescriptor.LocalColorTableFlag) 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. // Skip the frame indices. Pixels length + mincode size.
@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global; gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
} }
if (this.imageDescriptor.LocalColorTableFlag if (this.imageDescriptor.LocalColorTableFlag
@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local; 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. // Graphics control extensions is optional.
if (this.graphicsControlExtension != default) if (this.graphicsControlExtension != default)
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
} }
@ -751,14 +779,21 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (this.logicalScreenDescriptor.GlobalColorTableFlag) if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{ {
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
if (globalColorTableLength > 0) if (globalColorTableLength > 0)
{ {
this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean); 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()); 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.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; 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.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
@ -36,7 +40,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// The quantizer used to generate the color palette. /// The quantizer used to generate the color palette.
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private IQuantizer? quantizer;
/// <summary>
/// Whether the quantizer was supplied via options.
/// </summary>
private readonly bool hasQuantizer;
/// <summary> /// <summary>
/// The color table mode: Global or local. /// The color table mode: Global or local.
@ -64,6 +73,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
this.skipMetadata = encoder.SkipMetadata; this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer; this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode; this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
} }
@ -88,6 +98,21 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Quantize the image returning a palette. // Quantize the image returning a palette.
IndexedImageFrame<TPixel>? quantized; 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)) using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{ {
if (useGlobalTable) if (useGlobalTable)
@ -109,7 +134,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
WriteHeader(stream); WriteHeader(stream);
// Write the LSD. // 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); this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream);
if (useGlobalTable) if (useGlobalTable)
@ -141,6 +172,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{ {
PaletteQuantizer<TPixel> paletteQuantizer = default; PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false; 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++) for (int i = 0; i < image.Frames.Count; i++)
{ {
// Gather the metadata for this frame. // 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 // since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette. // multi frame gifs using a global palette.
hasPaletteQuantizer = true; 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. // Clean up for the next run.
quantized.Dispose(); if (quantized != previousQuantized)
{
quantized.Dispose();
}
} }
previousQuantized.Dispose();
indices.Dispose();
if (hasPaletteQuantizer) if (hasPaletteQuantizer)
{ {
paletteQuantizer.Dispose(); paletteQuantizer.Dispose();
@ -176,47 +221,55 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
int frameIndex, int frameIndex,
bool useLocal, bool useLocal,
GifFrameMetadata? metadata, GifFrameMetadata? metadata,
Buffer2D<byte> indices,
ref IndexedImageFrame<TPixel> previousQuantized,
ref IndexedImageFrame<TPixel> quantized, ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> paletteQuantizer) ref PaletteQuantizer<TPixel> globalPaletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The first frame has already been quantized so we do not need to do so again. // The first frame has already been quantized so we do not need to do so again.
int transparencyIndex = -1;
if (frameIndex > 0) if (frameIndex > 0)
{ {
if (useLocal) if (useLocal)
{ {
// Reassign using the current frame and details. // Reassign using the current frame and details.
QuantizerOptions? options = null; if (metadata?.DecodedLocalColorTable.Length > 0)
int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
{ {
options = new() // We can use the color data from the decoded metadata here.
{ // We avoid dithering by default to preserve the original colors.
Dither = this.quantizer.Options.Dither, PaletteQuantizer localQuantizer = new(metadata.DecodedLocalColorTable, new() { Dither = null });
DitherScale = this.quantizer.Options.DitherScale, using IQuantizer<TPixel> frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, localQuantizer.Options);
MaxColors = colorTableLength 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 else
{ {
// Quantize the image using the global palette. // 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); this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
} }
else
// Do we have extension information to write?
int index = GetTransparentIndex(quantized);
if (metadata != null || index > -1)
{ {
this.WriteGraphicalControlExtension(metadata ?? new(), index, stream); transparencyIndex = GetTransparentIndex(quantized, metadata);
} }
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
this.WriteImageDescriptor(frame, useLocal, stream); this.WriteImageDescriptor(frame, useLocal, stream);
if (useLocal) if (useLocal)
@ -224,18 +277,103 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteColorTable(quantized, stream); 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> /// <summary>
/// Returns the index of the most transparent color in the palette. /// Returns the index of the most transparent color in the palette.
/// </summary> /// </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> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns> /// <returns>
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized) private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Transparent pixels are much more likely to be found at the end of a palette. // 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; return index;
} }
@ -271,14 +414,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="width">The image width.</param> /// <param name="width">The image width.</param>
/// <param name="height">The image height.</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="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor( private void WriteLogicalScreenDescriptor(
ImageMetadata metadata, ImageMetadata metadata,
int width, int width,
int height, int height,
int transparencyIndex, int backgroundIndex,
bool useGlobalTable, bool useGlobalTable,
Stream stream) Stream stream)
{ {
@ -316,7 +459,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
width: (ushort)width, width: (ushort)width,
height: (ushort)height, height: (ushort)height,
packed: packedValue, packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex), backgroundColorIndex: unchecked((byte)backgroundIndex),
ratio); ratio);
Span<byte> buffer = stackalloc byte[20]; 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="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param> /// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</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( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod, disposalMethod: metadata!.DisposalMethod,
transparencyFlag: transparencyIndex > -1); transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new( GifGraphicControlExtension extension = new(
packed: packedValue, packed: packedValue,
delayTime: (ushort)metadata.FrameDelay, delayTime: (ushort)metadata.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex)); transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream); this.WriteExtension(extension, stream);
} }
@ -521,13 +674,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the image pixel data to the stream. /// Writes the image pixel data to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="indices">The <see cref="Buffer2D{Byte}"/> containing indexed pixels.</param>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream) private void WriteImageData(Buffer2D<byte> indices, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth); 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) private GifFrameMetadata(GifFrameMetadata other)
{ {
this.ColorTableMode = other.ColorTableMode; this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMethod = other.DisposalMethod;
if (other.DecodedLocalColorTable.Length > 0)
{
this.DecodedLocalColorTable = other.DecodedLocalColorTable.ToArray();
}
this.HasTransparency = other.HasTransparency;
this.TransparencyIndex = other.TransparencyIndex;
} }
/// <summary> /// <summary>
@ -33,11 +40,21 @@ public class GifFrameMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; } public GifColorTableMode ColorTableMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the length of the color table. /// Gets the decoded global color table, if any.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the /// </summary>
/// image frame. 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> /// </summary>
public int ColorTableLength { get; set; } public byte TransparencyIndex { get; set; }
/// <summary> /// <summary>
/// Gets or sets the frame delay for animated images. /// 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.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode; 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++) for (int i = 0; i < other.Comments.Count; i++)
{ {
@ -45,9 +50,15 @@ public class GifMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; } public GifColorTableMode ColorTableMode { get; set; }
/// <summary> /// <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> /// </summary>
public int GlobalColorTableLength { get; set; } public byte BackgroundColor { get; set; }
/// <summary> /// <summary>
/// Gets or sets 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

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

@ -17,14 +17,16 @@ public static partial class MetadataExtensions
/// </summary> /// </summary>
/// <param name="source">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns> /// <returns>The <see cref="GifMetadata"/>.</returns>
public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance); public static GifMetadata GetGifMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary> /// <summary>
/// Gets the gif format specific metadata for the image frame. /// Gets the gif format specific metadata for the image frame.
/// </summary> /// </summary>
/// <param name="source">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns> /// <returns>The <see cref="GifFrameMetadata"/>.</returns>
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance); public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary> /// <summary>
/// Gets the gif format specific metadata for the image frame. /// Gets the gif format specific metadata for the image frame.
@ -38,5 +40,6 @@ public static partial class MetadataExtensions
/// <returns> /// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>. /// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns> /// </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 // row: a d
// The Paeth function predicts d to be whichever of a, b, or c is nearest to // The Paeth function predicts d to be whichever of a, b, or c is nearest to
// p = a + b - c. // 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) else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{ {
@ -50,7 +50,7 @@ internal static class PaethFilter
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); 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)); Vector128<short> smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
// Paeth breaks ties favoring a over b over c. // Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
// Note `_epi8`: we need addition to wrap modulo 255. // Note `_epi8`: we need addition to wrap modulo 255.
d = Sse2.Add(d, nearest); d = Sse2.Add(d, nearest);
@ -143,8 +143,8 @@ internal static class PaethFilter
Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb)); Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c. // Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte()); Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte()); Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, uint bytesPerPixel) 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> /// </summary>
public class PngEncoder : QuantizingImageEncoder 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> /// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel). /// 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. /// 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder
/// <summary> /// <summary>
/// Gets the quantizer used to generate the color palette. /// Gets the quantizer used to generate the color palette.
/// </summary> /// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree; public IQuantizer? Quantizer { get; init; }
/// <summary> /// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes. /// 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.Advanced;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -12,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// </summary> /// </summary>
public class TiffEncoder : QuantizingImageEncoder public class TiffEncoder : QuantizingImageEncoder
{ {
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoder"/> class.
/// </summary>
public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
/// <summary> /// <summary>
/// Gets the number of bits per pixel. /// Gets the number of bits per pixel.
/// </summary> /// </summary>

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

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -85,7 +86,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation; this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy; this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel; this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor; this.HorizontalPredictor = options.HorizontalPredictor;
@ -320,7 +321,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{ {
int sz = ExifWriter.WriteValue(entry, buffer, 0); int sz = ExifWriter.WriteValue(entry, buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(buffer.Slice(0, sz)); writer.WritePadded(buffer[..sz]);
} }
else 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), /// 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! /// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
/// </summary> /// </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) internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{ {
bool swapped = false; bool swapped = false;
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup)) if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{ {
(destination.FastMemoryGroup, source.FastMemoryGroup) = (destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup);
(source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap(); destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap(); source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true; swapped = true;
@ -201,7 +203,6 @@ public sealed class Buffer2D<T> : IDisposable
} }
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y) => private void ThrowYOutOfRangeException(int y)
throw new ArgumentOutOfRangeException( => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
$"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> /// </summary>
public float DitherScale public float DitherScale
{ {
get { return this.ditherScale; } get => this.ditherScale;
set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
} }
/// <summary> /// <summary>
@ -35,7 +35,7 @@ public class QuantizerOptions
/// </summary> /// </summary>
public int MaxColors public int MaxColors
{ {
get { return this.maxColors; } get => this.maxColors;
set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.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(); GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode; GifColorTableMode colorMode = metaData.ColorTableMode;
int maxColors;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.DecodedGlobalColorTable.Length;
}
else
{
maxColors = frameMetadata.DecodedLocalColorTable.Length;
}
GifEncoder encoder = new() GifEncoder encoder = new()
{ {
ColorTableMode = colorMode, ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
}; };
image.Save(outStream, encoder); image.Save(outStream, encoder);
@ -187,15 +198,31 @@ public class GifEncoderTests
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors. // 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++) for (int i = 0; i < image.Frames.Count; i++)
{ {
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.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(iMeta.FrameDelay, cMeta.FrameDelay);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
} }
image.Dispose(); image.Dispose();

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

@ -11,21 +11,22 @@ public class GifFrameMetadataTests
[Fact] [Fact]
public void CloneIsDeep() public void CloneIsDeep()
{ {
var meta = new GifFrameMetadata GifFrameMetadata meta = new()
{ {
FrameDelay = 1, FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground, 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.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.ColorTableLength = 1; clone.DecodedLocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using Microsoft.CodeAnalysis;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -35,7 +34,7 @@ public class GifMetadataTests
{ {
RepeatCount = 1, RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global, ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2, DecodedGlobalColorTable = new[] { Color.Black, Color.White },
Comments = new List<string> { "Foo" } Comments = new List<string> { "Foo" }
}; };
@ -43,11 +42,12 @@ public class GifMetadataTests
clone.RepeatCount = 2; clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local; clone.ColorTableMode = GifColorTableMode.Local;
clone.GlobalColorTableLength = 1; clone.DecodedGlobalColorTable = new[] { Color.Black };
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount)); Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode)); 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.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(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(); GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata();
Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode); 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(frameDelay, gifFrameMetadata.FrameDelay);
Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod); 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 source = provider.GetImage();
using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height); using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height);
// Giphy.gif has 5 frames // Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0); ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1); ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
@ -289,7 +290,7 @@ public abstract partial class ImageFrameCollectionTests
// Drop the original empty root frame: // Drop the original empty root frame:
dest.Frames.RemoveFrame(0); dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false);
dest.CompareToOriginal(provider); dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++) 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.DisposalMethod, bData.DisposalMethod);
Assert.Equal(aData.FrameDelay, bData.FrameDelay); 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.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -22,17 +23,17 @@ public class ImageFrameMetadataTests
const int colorTableLength = 128; const int colorTableLength = 128;
const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground;
var metaData = new ImageFrameMetadata(); ImageFrameMetadata metaData = new();
GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata();
gifFrameMetadata.FrameDelay = frameDelay; gifFrameMetadata.FrameDelay = frameDelay;
gifFrameMetadata.ColorTableLength = colorTableLength; gifFrameMetadata.DecodedLocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray();
gifFrameMetadata.DisposalMethod = disposalMethod; gifFrameMetadata.DisposalMethod = disposalMethod;
var clone = new ImageFrameMetadata(metaData); ImageFrameMetadata clone = new(metaData);
GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata();
Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength); Assert.Equal(colorTableLength, cloneGifFrameMetadata.DecodedLocalColorTable.Length);
Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod);
} }
@ -40,19 +41,19 @@ public class ImageFrameMetadataTests
public void CloneIsDeep() public void CloneIsDeep()
{ {
// arrange // arrange
var exifProfile = new ExifProfile(); ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]); XmpProfile xmpProfile = new(Array.Empty<byte>());
var iccProfile = new IccProfile() IccProfile iccProfile = new()
{ {
Header = new IccProfileHeader() Header = new IccProfileHeader()
{ {
CmmType = "Unittest" CmmType = "Unittest"
} }
}; };
var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile(); IptcProfile iptcProfile = new();
var metaData = new ImageFrameMetadata() ImageFrameMetadata metaData = new()
{ {
XmpProfile = xmpProfile, XmpProfile = xmpProfile,
ExifProfile = exifProfile, ExifProfile = exifProfile,

Loading…
Cancel
Save