Browse Source

Enable dedup for png

pull/2588/head
James Jackson-South 2 years ago
parent
commit
a42f6b65da
  1. 11
      src/ImageSharp/Formats/AnimationUtilities.cs
  2. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  3. 2
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 2
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  5. 169
      src/ImageSharp/Formats/Png/PngEncoderCore.cs

11
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;
}

2
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

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

@ -245,8 +245,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> 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<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)

2
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

169
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
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// Any explicit quantized transparent index provided by the background color.
/// </summary>
private int derivedTransparencyIndex = -1;
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary>
@ -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<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null);
IndexedImageFrame<TPixel>? 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<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
uint increment = 0;
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)
{
currentFrame = image.Frames[i];
frameMetadata = GetPngFrameMetadata(currentFrame);
ImageFrame<TPixel>? 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
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="bounds">The area of interest within the frame.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
PngMetadata metadata,
ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette);
IndexedImageFrame<TPixel>? 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.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageFrame">The image frame.</param>
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="bounds">The frame area of interest.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
private FrameControl WriteFrameControlChunk<TPixel>(Stream stream, ImageFrame<TPixel> imageFrame, uint sequenceNumber)
where TPixel : unmanaged, IPixel<TPixel>
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
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="pixels">The frame.</param>
/// <param name="frame">The image frame.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
/// <param name="isFrame">Is writing fdAT or IDAT.</param>
private uint WriteDataChunks<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
private uint WriteDataChunks<TPixel>(FrameControl frameControl, Buffer2DRegion<TPixel> frame, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="quantized">The quantized pixels.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
private void EncodePixels<TPixel>(Buffer2DRegion<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using IMemoryOwner<byte> attemptBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
pixels.ProcessPixelRows(accessor =>
Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan();
for (int y = 0; y < pixels.Height; y++)
{
Span<byte> filter = filterBuffer.GetSpan();
Span<byte> 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();
}
}
/// <summary>
/// Interlaced encoding the pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="frame">The image frame.</param>
/// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> frame, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(Buffer2DRegion<TPixel> pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = (int)frameControl.XMax;
int height = (int)frameControl.YMax;
Buffer2D<TPixel> 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<byte> filter = filterBuffer.GetSpan();
Span<byte> 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<TPixel> srcRow = pixelBuffer.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass])
Span<TPixel> 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(FrameControl frameControl, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodeAdam7IndexedPixels<TPixel>(IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<byte> filter = filterBuffer.GetSpan();
Span<byte> 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<byte> 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
/// <param name="bitDepth">The bits per component.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="bounds">The frame area of interest.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder,
@ -1462,6 +1462,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
byte bitDepth,
PngMetadata metadata,
ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -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<TPixel> paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1);
using PaletteQuantizer<TPixel> 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<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
return frameQuantizer.QuantizeFrame(frame, bounds);
}
/// <summary>

Loading…
Cancel
Save