diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 23e6ed094f..45819b751a 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -291,20 +291,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream); - // TODO: Consider an optimization that trims down the buffer to the minimum size required. - // We would use a process similar to entropy crop where we trim the buffer from the edges - // until we hit a non-transparent pixel. - this.WriteImageDescriptor(frame, useLocal, 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 buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices; + + // Trim down the buffer to the minimum size required. + Buffer2DRegion region = TrimTransparentPixels(buffer, transparencyIndex); + this.WriteImageDescriptor(region.Rectangle, useLocal, stream); if (useLocal) { this.WriteColorTable(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 buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices; - this.WriteImageData(buffer, stream); + this.WriteImageData(region, stream); // Swap the buffers. (quantized, previousQuantized) = (previousQuantized, quantized); @@ -386,6 +386,44 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } } + private static Buffer2DRegion TrimTransparentPixels(Buffer2D buffer, int transparencyIndex) + { + if (transparencyIndex < 0) + { + return buffer.GetRegion(); + } + + byte trimmableIndex = unchecked((byte)transparencyIndex); + + int top = int.MaxValue; + int bottom = int.MinValue; + int left = int.MaxValue; + int right = int.MinValue; + + for (int y = 0; y < buffer.Height; y++) + { + Span rowSpan = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < rowSpan.Length; x++) + { + if (rowSpan[x] != trimmableIndex) + { + top = Math.Min(top, y); + bottom = Math.Max(bottom, y); + left = Math.Min(left, x); + right = Math.Max(right, x); + } + } + } + + if (top == int.MaxValue || bottom == int.MinValue) + { + // No valid rectangle found + return buffer.GetRegion(); + } + + return buffer.GetRegion(Rectangle.FromLTRB(left, top, right, bottom)); + } + /// /// Returns the index of the most transparent color in the palette. /// @@ -619,7 +657,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } IMemoryOwner? owner = null; - Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation + Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation if (extensionSize > 128) { owner = this.memoryAllocator.Allocate(extensionSize + 3); @@ -642,14 +680,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } /// - /// Writes the image descriptor to the stream. + /// Writes the image frame descriptor to the stream. /// - /// The pixel format. - /// The to be encoded. + /// The frame location and size. /// Whether to use the global color table. /// The stream to write to. - private void WriteImageDescriptor(ImageFrame image, bool hasColorTable, Stream stream) - where TPixel : unmanaged, IPixel + private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream) { byte packedValue = GifImageDescriptor.GetPackedValue( localColorTableFlag: hasColorTable, @@ -658,10 +694,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals localColorTableSize: this.bitDepth - 1); GifImageDescriptor descriptor = new( - left: 0, - top: 0, - width: (ushort)image.Width, - height: (ushort)image.Height, + left: (ushort)rectangle.X, + top: (ushort)rectangle.Y, + width: (ushort)rectangle.Width, + height: (ushort)rectangle.Height, packed: packedValue); Span buffer = stackalloc byte[20]; @@ -697,9 +733,9 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// Writes the image pixel data to the stream. /// - /// The containing indexed pixels. + /// The containing indexed pixels. /// The stream to write to. - private void WriteImageData(Buffer2D indices, Stream stream) + private void WriteImageData(Buffer2DRegion indices, Stream stream) { using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth); encoder.Encode(indices, stream); diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index 5253c0978a..4b40c44e45 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable /// /// The 2D buffer of indexed pixels. /// The stream to write to. - public void Encode(Buffer2D indexedPixels, Stream stream) + public void Encode(Buffer2DRegion indexedPixels, Stream stream) { // Write "initial code size" byte stream.WriteByte((byte)this.initialCodeSize); @@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable /// The 2D buffer of indexed pixels. /// The initial bits. /// The stream to write to. - private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream) + private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream) { // Set up the globals: globalInitialBits - initial number of bits this.globalInitialBits = initialBits;