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()); int m = Avx2.MoveMask(neq.AsByte());
if (m != 0) 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. // 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 diff position + 1. // The right is the max of the previously found right side and the end position.
int diff = (int)(i + (uint)(BitOperations.TrailingZeroCount(m) / size)); int start = i + (BitOperations.TrailingZeroCount(m) / size);
left = Math.Min(left, diff); int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size));
right = Math.Max(right, diff + 1); left = Math.Min(left, start);
right = Math.Max(right, end);
hasRowDiff = true; hasRowDiff = true;
hasDiff = 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.GlobalColorTable = colorTable;
} }
} }
this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
} }
private unsafe struct ScratchBuffer 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; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // 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()); using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++) 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, ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10), Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10),
DisposalMode = GetMode(source.DisposalMethod), DisposalMode = GetMode(source.DisposalMethod),
BlendMode = FrameBlendMode.Source, BlendMode = FrameBlendMode.Over,
}; };
private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch 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;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
@ -118,6 +119,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
private IQuantizer? quantizer; private IQuantizer? quantizer;
/// <summary>
/// Any explicit quantized transparent index provided by the background color.
/// </summary>
private int derivedTransparencyIndex = -1;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class. /// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary> /// </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. // 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.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
@ -180,44 +190,51 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount); 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. // Write the first frame.
FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0); PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); 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. // Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray(); ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames. // Write following frames.
uint increment = 0; 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++) for (int i = 1; i < image.Frames.Count; i++)
{ {
currentFrame = image.Frames[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) if (clearTransparency)
{ {
// Dispose of previous clone and reassign. ClearTransparentPixels(encodingFrame);
clonedFrame?.Dispose();
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame);
} }
// Each frame control sequence number must be incremented by the // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
// number of frame data chunks that follow. frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, (uint)i + increment);
frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment);
// Dispose of previous quantized frame and reassign. // Dispose of previous quantized frame and reassign.
quantized?.Dispose(); quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette); quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
} }
} }
else else
{ {
FrameControl frameControl = new((uint)this.width, (uint)this.height); 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); this.WriteEndChunk(stream);
@ -317,15 +334,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</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> /// <param name="previousPalette">Any previously derived palette.</param>
/// <returns>The quantized image.</returns> /// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>( private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
PngMetadata metadata, PngMetadata metadata,
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette) ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel> 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); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized; return quantized;
} }
@ -1033,20 +1052,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Writes the animation control chunk to the stream. /// Writes the animation control chunk to the stream.
/// </summary> /// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <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> /// <param name="sequenceNumber">The frame sequence number.</param>
private FrameControl WriteFrameControlChunk<TPixel>(Stream stream, ImageFrame<TPixel> imageFrame, uint sequenceNumber) private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, Rectangle bounds, uint sequenceNumber)
where TPixel : unmanaged, IPixel<TPixel>
{ {
PngFrameMetadata frameMetadata = GetPngFrameMetadata(imageFrame);
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
FrameControl fcTL = new( FrameControl fcTL = new(
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,
width: (uint)imageFrame.Width, width: (uint)bounds.Width,
height: (uint)imageFrame.Height, height: (uint)bounds.Height,
xOffset: 0, xOffset: (uint)bounds.Left,
yOffset: 0, yOffset: (uint)bounds.Top,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod, disposeOperation: frameMetadata.DisposalMethod,
@ -1064,11 +1080,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param> /// <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="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
/// <param name="isFrame">Is writing fdAT or IDAT.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[] buffer; byte[] buffer;
@ -1082,16 +1098,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
if (quantized is not null) if (quantized is not null)
{ {
this.EncodeAdam7IndexedPixels(frameControl, quantized, deflateStream); this.EncodeAdam7IndexedPixels(quantized, deflateStream);
} }
else else
{ {
this.EncodeAdam7Pixels(frameControl, pixels, deflateStream); this.EncodeAdam7Pixels(frame, deflateStream);
} }
} }
else 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. /// Encodes the pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param> /// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="pixels">The pixels.</param> /// <param name="quantized">The quantized pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = (int)frameControl.Width; int bytesPerScanline = this.CalculateScanlineLength(pixels.Width);
int height = (int)frameControl.Height;
int bytesPerScanline = this.CalculateScanlineLength(width);
int filterLength = bytesPerScanline + 1; int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline); this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean); using IMemoryOwner<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using IMemoryOwner<byte> attemptBuffer = 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(); this.CollectAndFilterPixelRow(pixels.DangerousGetRowSpan(y), ref filter, ref attempt, quantized, y);
Span<byte> attempt = attemptBuffer.GetSpan(); deflateStream.Write(filter);
for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++) this.SwapScanlineBuffers();
{ }
this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter);
this.SwapScanlineBuffers();
}
});
} }
/// <summary> /// <summary>
/// Interlaced encoding the pixels. /// Interlaced encoding the pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param> /// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="frame">The image frame.</param>
/// <param name="deflateStream">The deflate stream.</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> 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++) for (int pass = 0; pass < 7; pass++)
{ {
int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; int startRow = Adam7.FirstRow[pass];
int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int startCol = Adam7.FirstColumn[pass];
int blockWidth = Adam7.ComputeBlockWidth(width, pass); int blockWidth = Adam7.ComputeBlockWidth(pixels.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1 int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8 ? ((blockWidth * this.bitDepth) + 7) / 8
@ -1220,13 +1225,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> filter = filterBuffer.GetSpan(); Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.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 // Collect pixel data
Span<TPixel> srcRow = pixelBuffer.DangerousGetRowSpan(row); Span<TPixel> srcRow = pixels.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) for (int col = startCol, i = 0; col < pixels.Width; col += Adam7.ColumnIncrement[pass], i++)
{ {
block[i++] = srcRow[col]; block[i] = srcRow[col];
} }
// Encode data // Encode data
@ -1244,19 +1249,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the quantized (indexed, with palette) pixels. /// Interlaced encoding the quantized (indexed, with palette) pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="quantized">The quantized.</param> /// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = (int)frameControl.Width;
int endRow = (int)frameControl.YMax;
for (int pass = 0; pass < 7; pass++) for (int pass = 0; pass < 7; pass++)
{ {
int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; int startRow = Adam7.FirstRow[pass];
int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int startCol = Adam7.FirstColumn[pass];
int blockWidth = Adam7.ComputeBlockWidth(width, pass); int blockWidth = Adam7.ComputeBlockWidth(quantized.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1 int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8 ? ((blockWidth * this.bitDepth) + 7) / 8
@ -1274,16 +1276,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> filter = filterBuffer.GetSpan(); Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.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 // Collect data
ReadOnlySpan<byte> srcRow = quantized.DangerousGetRowSpan(row); ReadOnlySpan<byte> srcRow = quantized.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; for (int col = startCol, i = 0; col < quantized.Width; col += Adam7.ColumnIncrement[pass], i++)
col < frameControl.XMax;
col += Adam7.ColumnIncrement[pass])
{ {
block[i] = srcRow[col]; block[i] = srcRow[col];
i++;
} }
// Encode data // Encode data
@ -1455,6 +1454,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="bitDepth">The bits per component.</param> /// <param name="bitDepth">The bits per component.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</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> /// <param name="previousPalette">Any previously derived palette.</param>
private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>( private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder, QuantizingImageEncoder encoder,
@ -1462,6 +1462,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
byte bitDepth, byte bitDepth,
PngMetadata metadata, PngMetadata metadata,
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette) ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -1473,9 +1474,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (previousPalette is not null) if (previousPalette is not null)
{ {
// Use the previously derived palette created by quantizing the root frame to quantize the current frame. // 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); 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. // 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) if (metadata.ColorTable is not null)
{ {
// Use the provided palette. The caller is responsible for setting values. // We can use the color data from the decoded metadata here.
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value); // 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 else
{ {
@ -1496,7 +1503,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration); using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); return frameQuantizer.QuantizeFrame(frame, bounds);
} }
/// <summary> /// <summary>

Loading…
Cancel
Save