From a42f6b65daf61c19e07ee3ead2b5c128f80f115d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2023 13:41:46 +1000 Subject: [PATCH] Enable dedup for png --- src/ImageSharp/Formats/AnimationUtilities.cs | 11 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 + src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 - .../Formats/Gif/MetadataExtensions.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 169 +++++++++--------- 5 files changed, 97 insertions(+), 89 deletions(-) diff --git a/src/ImageSharp/Formats/AnimationUtilities.cs b/src/ImageSharp/Formats/AnimationUtilities.cs index 1bca34eae..ee9a85ac4 100644 --- a/src/ImageSharp/Formats/AnimationUtilities.cs +++ b/src/ImageSharp/Formats/AnimationUtilities.cs @@ -85,11 +85,12 @@ internal static class AnimationUtilities int m = Avx2.MoveMask(neq.AsByte()); if (m != 0) { - // If is diff is found, the left side is marked by the min of previously found left side and the diff position. - // The right is the max of the previously found right side and the diff position + 1. - int diff = (int)(i + (uint)(BitOperations.TrailingZeroCount(m) / size)); - left = Math.Min(left, diff); - right = Math.Max(right, diff + 1); + // If is diff is found, the left side is marked by the min of previously found left side and the start position. + // The right is the max of the previously found right side and the end position. + int start = i + (BitOperations.TrailingZeroCount(m) / size); + int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size)); + left = Math.Min(left, start); + right = Math.Max(right, end); hasRowDiff = true; hasDiff = true; } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index bc41c89dc..aecbbbbc7 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -797,6 +797,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals this.gifMetadata.GlobalColorTable = colorTable; } } + + this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex; } private unsafe struct ScratchBuffer diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 2bc3e53f7..24bb3c00e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -245,8 +245,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. - // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate - // frames using both local and global palettes. using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); for (int i = 1; i < image.Frames.Count; i++) diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index c7f9f84c8..1b9b6ac58 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -83,7 +83,7 @@ public static partial class MetadataExtensions ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), DisposalMode = GetMode(source.DisposalMethod), - BlendMode = FrameBlendMode.Source, + BlendMode = FrameBlendMode.Over, }; private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a779718a0..016c42233 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; @@ -118,6 +119,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// private IQuantizer? quantizer; + /// + /// Any explicit quantized transparent index provided by the background color. + /// + private int derivedTransparencyIndex = -1; + /// /// Initializes a new instance of the class. /// @@ -164,7 +170,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } // Do not move this. We require an accurate bit depth for the header chunk. - IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null); + IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth( + pngMetadata, + currentFrame, + currentFrame.Bounds(), + null); this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); @@ -180,44 +190,51 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount); - // TODO: We should attempt to optimize the output by clipping the indexed result to - // non-transparent bounds. That way we can assign frame control bounds and encode - // less data. See GifEncoder for the implementation there. - // Write the first frame. - FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0); - this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); + PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); + PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); // Capture the global palette for reuse on subsequent frames. ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); // Write following frames. uint increment = 0; + ImageFrame previousFrame = image.Frames.RootFrame; + + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + for (int i = 1; i < image.Frames.Count; i++) { currentFrame = image.Frames[i]; + frameMetadata = GetPngFrameMetadata(currentFrame); + + ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; + (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero); + if (clearTransparency) { - // Dispose of previous clone and reassign. - clonedFrame?.Dispose(); - currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame); + ClearTransparentPixels(encodingFrame); } - // Each frame control sequence number must be incremented by the - // number of frame data chunks that follow. - frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment); + // Each frame control sequence number must be incremented by the number of frame data chunks that follow. + frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, (uint)i + increment); // Dispose of previous quantized frame and reassign. quantized?.Dispose(); - quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette); - increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); + quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); + increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true); + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMethod; } } else { FrameControl frameControl = new((uint)this.width, (uint)this.height); - this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); } this.WriteEndChunk(stream); @@ -317,15 +334,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The type of the pixel. /// The image metadata. /// The frame to quantize. + /// The area of interest within the frame. /// Any previously derived palette. /// The quantized image. private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( PngMetadata metadata, ImageFrame frame, + Rectangle bounds, ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { - IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette); + IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, bounds, previousPalette); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -1033,20 +1052,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Writes the animation control chunk to the stream. /// /// The containing image data. - /// The image frame. + /// The frame metadata. + /// The frame area of interest. /// The frame sequence number. - private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber) - where TPixel : unmanaged, IPixel + private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, Rectangle bounds, uint sequenceNumber) { - PngFrameMetadata frameMetadata = GetPngFrameMetadata(imageFrame); - - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. FrameControl fcTL = new( sequenceNumber: sequenceNumber, - width: (uint)imageFrame.Width, - height: (uint)imageFrame.Height, - xOffset: 0, - yOffset: 0, + width: (uint)bounds.Width, + height: (uint)bounds.Height, + xOffset: (uint)bounds.Left, + yOffset: (uint)bounds.Top, delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, disposeOperation: frameMetadata.DisposalMethod, @@ -1064,11 +1080,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The pixel format. /// The frame control - /// The frame. + /// The image frame. /// The quantized pixel data. Can be null. /// The stream. /// Is writing fdAT or IDAT. - private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame) + private uint WriteDataChunks(FrameControl frameControl, Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -1082,16 +1098,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { if (quantized is not null) { - this.EncodeAdam7IndexedPixels(frameControl, quantized, deflateStream); + this.EncodeAdam7IndexedPixels(quantized, deflateStream); } else { - this.EncodeAdam7Pixels(frameControl, pixels, deflateStream); + this.EncodeAdam7Pixels(frame, deflateStream); } } else { - this.EncodePixels(frameControl, pixels, quantized, deflateStream); + this.EncodePixels(frame, quantized, deflateStream); } } @@ -1156,54 +1172,43 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Encodes the pixels. /// /// The type of the pixel. - /// The frame control - /// The pixels. - /// The quantized pixels span. + /// The image frame pixel buffer. + /// The quantized pixels. /// The deflate stream. - private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = (int)frameControl.Width; - int height = (int)frameControl.Height; - - int bytesPerScanline = this.CalculateScanlineLength(width); + int bytesPerScanline = this.CalculateScanlineLength(pixels.Width); int filterLength = bytesPerScanline + 1; this.AllocateScanlineBuffers(bytesPerScanline); using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); - pixels.ProcessPixelRows(accessor => + Span filter = filterBuffer.GetSpan(); + Span attempt = attemptBuffer.GetSpan(); + for (int y = 0; y < pixels.Height; y++) { - Span filter = filterBuffer.GetSpan(); - Span attempt = attemptBuffer.GetSpan(); - for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++) - { - this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); - deflateStream.Write(filter); - this.SwapScanlineBuffers(); - } - }); + this.CollectAndFilterPixelRow(pixels.DangerousGetRowSpan(y), ref filter, ref attempt, quantized, y); + deflateStream.Write(filter); + this.SwapScanlineBuffers(); + } } /// /// Interlaced encoding the pixels. /// /// The type of the pixel. - /// The frame control - /// The image frame. + /// The image frame pixel buffer. /// The deflate stream. - private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(Buffer2DRegion pixels, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = (int)frameControl.XMax; - int height = (int)frameControl.YMax; - Buffer2D pixelBuffer = frame.PixelBuffer; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; - int blockWidth = Adam7.ComputeBlockWidth(width, pass); + int startRow = Adam7.FirstRow[pass]; + int startCol = Adam7.FirstColumn[pass]; + int blockWidth = Adam7.ComputeBlockWidth(pixels.Width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 ? ((blockWidth * this.bitDepth) + 7) / 8 @@ -1220,13 +1225,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; row < height; row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < pixels.Height; row += Adam7.RowIncrement[pass]) { // Collect pixel data - Span srcRow = pixelBuffer.DangerousGetRowSpan(row); - for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) + Span srcRow = pixels.DangerousGetRowSpan(row); + for (int col = startCol, i = 0; col < pixels.Width; col += Adam7.ColumnIncrement[pass], i++) { - block[i++] = srcRow[col]; + block[i] = srcRow[col]; } // Encode data @@ -1244,19 +1249,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Interlaced encoding the quantized (indexed, with palette) pixels. /// /// The type of the pixel. - /// The frame control /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = (int)frameControl.Width; - int endRow = (int)frameControl.YMax; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; - int blockWidth = Adam7.ComputeBlockWidth(width, pass); + int startRow = Adam7.FirstRow[pass]; + int startCol = Adam7.FirstColumn[pass]; + int blockWidth = Adam7.ComputeBlockWidth(quantized.Width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 ? ((blockWidth * this.bitDepth) + 7) / 8 @@ -1274,16 +1276,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < quantized.Height; row += Adam7.RowIncrement[pass]) { // Collect data ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); - for (int col = startCol, i = 0; - col < frameControl.XMax; - col += Adam7.ColumnIncrement[pass]) + for (int col = startCol, i = 0; col < quantized.Width; col += Adam7.ColumnIncrement[pass], i++) { block[i] = srcRow[col]; - i++; } // Encode data @@ -1455,6 +1454,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The bits per component. /// The image metadata. /// The frame to quantize. + /// The frame area of interest. /// Any previously derived palette. private IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, @@ -1462,6 +1462,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable byte bitDepth, PngMetadata metadata, ImageFrame frame, + Rectangle bounds, ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { @@ -1473,9 +1474,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable if (previousPalette is not null) { // Use the previously derived palette created by quantizing the root frame to quantize the current frame. - using PaletteQuantizer paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1); + using PaletteQuantizer paletteQuantizer = new( + this.configuration, + this.quantizer!.Options, + previousPalette.Value, + this.derivedTransparencyIndex); paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); - return paletteQuantizer.QuantizeFrame(frame, frame.Bounds()); + return paletteQuantizer.QuantizeFrame(frame, bounds); } // Use the metadata to determine what quantization depth to use if no quantizer has been set. @@ -1483,8 +1488,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { if (metadata.ColorTable is not null) { - // Use the provided palette. The caller is responsible for setting values. - this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value); + // We can use the color data from the decoded metadata here. + // We avoid dithering by default to preserve the original colors. + this.derivedTransparencyIndex = metadata.ColorTable.Value.Span.IndexOf(Color.Transparent); + this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }, this.derivedTransparencyIndex); } else { @@ -1496,7 +1503,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); - return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + return frameQuantizer.QuantizeFrame(frame, bounds); } ///