diff --git a/src/ImageSharp/Formats/Png/Adam7.cs b/src/ImageSharp/Formats/Png/Adam7.cs
index 4e6485b55f..b392332d7a 100644
--- a/src/ImageSharp/Formats/Png/Adam7.cs
+++ b/src/ImageSharp/Formats/Png/Adam7.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@@ -31,6 +31,34 @@ internal static class Adam7
///
public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
+ ///
+ /// Gets the width of the block.
+ ///
+ /// The width.
+ /// The pass.
+ ///
+ /// The
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ComputeBlockWidth(int width, int pass)
+ {
+ return (width + ColumnIncrement[pass] - 1 - FirstColumn[pass]) / ColumnIncrement[pass];
+ }
+
+ ///
+ /// Gets the height of the block.
+ ///
+ /// The height.
+ /// The pass.
+ ///
+ /// The
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ComputeBlockHeight(int height, int pass)
+ {
+ return (height + RowIncrement[pass] - 1 - FirstRow[pass]) / RowIncrement[pass];
+ }
+
///
/// Returns the correct number of columns for each interlaced pass.
///
@@ -53,4 +81,4 @@ public static int ComputeColumns(int width, int passIndex)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
index ee1a823fd2..87fd2582a5 100644
--- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
@@ -35,7 +35,7 @@ internal interface IPngEncoderOptions
///
/// Gets the threshold of characters in text metadata, when compression should be used.
///
- int CompressTextThreshold { get; }
+ int TextCompressionThreshold { get; }
///
/// Gets the gamma value, that will be written the image.
@@ -52,5 +52,10 @@ internal interface IPngEncoderOptions
/// Gets the transparency threshold.
///
byte Threshold { get; }
+
+ ///
+ /// Gets a value indicating whether this instance should write an Adam7 interlaced image.
+ ///
+ PngInterlaceMode? InterlaceMethod { get; }
}
}
diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs
index d54a53c1c3..5a846ac3e2 100644
--- a/src/ImageSharp/Formats/Png/PngConstants.cs
+++ b/src/ImageSharp/Formats/Png/PngConstants.cs
@@ -52,7 +52,7 @@ internal static class PngConstants
};
///
- /// The header bytes as a big endian coded ulong.
+ /// The header bytes as a big-endian coded ulong.
///
public const ulong HeaderValue = 0x89504E470D0A1A0AUL;
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 74ead3938a..c7ffc46a79 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -31,7 +31,7 @@ internal sealed class PngDecoderCore
private readonly byte[] buffer = new byte[4];
///
- /// Reusable crc for validating chunks.
+ /// Reusable CRC for validating chunks.
///
private readonly Crc32 crc = new Crc32();
@@ -106,12 +106,7 @@ internal sealed class PngDecoderCore
private int currentRow = Adam7.FirstRow[0];
///
- /// The current pass for an interlaced PNG.
- ///
- private int pass;
-
- ///
- /// The current number of bytes read in the current scanline.
+ /// The current number of bytes read in the current scanline
///
private int currentRowBytesRead;
@@ -551,13 +546,15 @@ private void DecodePixelData(Stream compressedStream, ImageFrame
private void DecodeInterlacedPixelData(Stream compressedStream, ImageFrame image, PngMetadata pngMetadata)
where TPixel : struct, IPixel
{
+ int pass = 0;
+ int width = this.header.Width;
while (true)
{
- int numColumns = Adam7.ComputeColumns(this.header.Width, this.pass);
+ int numColumns = Adam7.ComputeColumns(width, pass);
if (numColumns == 0)
{
- this.pass++;
+ pass++;
// This pass contains no data; skip to next pass
continue;
@@ -605,23 +602,23 @@ private void DecodeInterlacedPixelData(Stream compressedStream, ImageFra
}
Span rowSpan = image.GetPixelRowSpan(this.currentRow);
- this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]);
+ this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
this.SwapBuffers();
- this.currentRow += Adam7.RowIncrement[this.pass];
+ this.currentRow += Adam7.RowIncrement[pass];
}
- this.pass++;
+ pass++;
this.previousScanline.Clear();
- if (this.pass < 7)
+ if (pass < 7)
{
- this.currentRow = Adam7.FirstRow[this.pass];
+ this.currentRow = Adam7.FirstRow[pass];
}
else
{
- this.pass = 0;
+ pass = 0;
break;
}
}
@@ -859,6 +856,7 @@ private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data)
pngMetadata.BitDepth = (PngBitDepth)this.header.BitDepth;
pngMetadata.ColorType = this.header.ColorType;
+ pngMetadata.InterlaceMethod = this.header.InterlaceMethod;
this.pngColorType = this.header.ColorType;
}
@@ -1202,7 +1200,6 @@ private bool TryReadChunkLength(out int result)
}
result = default;
-
return false;
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index 7ef465a485..3e46ad29ec 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -36,9 +36,10 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
public int CompressionLevel { get; set; } = 6;
///
- /// Gets or sets the threshold of characters in text metadata, when compression should be used. Defaults to 1024.
+ /// Gets or sets the threshold of characters in text metadata, when compression should be used.
+ /// Defaults to 1024.
///
- public int CompressTextThreshold { get; set; } = 1024;
+ public int TextCompressionThreshold { get; set; } = 1024;
///
/// Gets or sets the gamma value, that will be written the image.
@@ -47,14 +48,19 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
///
/// Gets or sets quantizer for reducing the color count.
- /// Defaults to the
+ /// Defaults to the .
///
public IQuantizer Quantizer { get; set; }
///
/// Gets or sets the transparency threshold.
///
- public byte Threshold { get; set; } = 255;
+ public byte Threshold { get; set; } = byte.MaxValue;
+
+ ///
+ /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
+ ///
+ public PngInterlaceMode? InterlaceMethod { get; set; }
///
/// Encodes the image to the specified stream from the .
@@ -65,7 +71,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
public void Encode(Image image, Stream stream)
where TPixel : struct, IPixel
{
- using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), this))
+ using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this)))
{
encoder.Encode(image, stream);
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 695c5c9f57..09575bb288 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -4,12 +4,10 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@@ -29,16 +27,9 @@ namespace SixLabors.ImageSharp.Formats.Png
internal sealed class PngEncoderCore : IDisposable
{
///
- /// The dictionary of available color types.
+ /// The maximum block size, defaults at 64k for uncompressed blocks.
///
- private static readonly Dictionary ColorTypes = new Dictionary()
- {
- [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
- [PngColorType.Rgb] = new byte[] { 8, 16 },
- [PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
- [PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
- [PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
- };
+ private const int MaxBlockSize = 65535;
///
/// Used the manage memory allocations.
@@ -48,12 +39,7 @@ internal sealed class PngEncoderCore : IDisposable
///
/// The configuration instance for the decoding operation.
///
- private Configuration configuration;
-
- ///
- /// The maximum block size, defaults at 64k for uncompressed blocks.
- ///
- private const int MaxBlockSize = 65535;
+ private readonly Configuration configuration;
///
/// Reusable buffer for writing general data.
@@ -66,44 +52,19 @@ internal sealed class PngEncoderCore : IDisposable
private readonly byte[] chunkDataBuffer = new byte[16];
///
- /// Reusable crc for validating chunks.
+ /// Reusable CRC for validating chunks.
///
private readonly Crc32 crc = new Crc32();
///
- /// The png filter method.
- ///
- private readonly PngFilterMethod pngFilterMethod;
-
- ///
- /// Gets or sets the CompressionLevel value.
- ///
- private readonly int compressionLevel;
-
- ///
- /// The threshold of characters in text metadata, when compression should be used.
- ///
- private readonly int compressTextThreshold;
-
- ///
- /// Gets or sets the alpha threshold value
+ /// The encoder options
///
- private readonly byte threshold;
+ private readonly PngEncoderOptions options;
///
- /// The quantizer for reducing the color count.
+ /// The bit depth.
///
- private IQuantizer quantizer;
-
- ///
- /// Gets or sets a value indicating whether to write the gamma chunk.
- ///
- private bool writeGamma;
-
- ///
- /// The png bit depth.
- ///
- private PngBitDepth? pngBitDepth;
+ private byte bitDepth;
///
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
@@ -111,14 +72,9 @@ internal sealed class PngEncoderCore : IDisposable
private bool use16Bit;
///
- /// The png color type.
- ///
- private PngColorType? pngColorType;
-
- ///
- /// Gets or sets the Gamma value
+ /// The number of bytes per pixel.
///
- private float? gamma;
+ private int bytesPerPixel;
///
/// The image width.
@@ -131,75 +87,46 @@ internal sealed class PngEncoderCore : IDisposable
private int height;
///
- /// The number of bits required to encode the colors in the png.
- ///
- private byte bitDepth;
-
- ///
- /// The number of bytes per pixel.
- ///
- private int bytesPerPixel;
-
- ///
- /// The number of bytes per scanline.
- ///
- private int bytesPerScanline;
-
- ///
- /// The previous scanline.
+ /// The raw data of previous scanline.
///
private IManagedByteBuffer previousScanline;
///
- /// The raw scanline.
+ /// The raw data of current scanline.
///
- private IManagedByteBuffer rawScanline;
+ private IManagedByteBuffer currentScanline;
///
- /// The filtered scanline result.
+ /// The common buffer for the filters.
///
- private IManagedByteBuffer result;
+ private IManagedByteBuffer filterBuffer;
///
- /// The buffer for the sub filter
+ /// The ext buffer for the sub filter, .
///
- private IManagedByteBuffer sub;
+ private IManagedByteBuffer subFilter;
///
- /// The buffer for the up filter
+ /// The ext buffer for the average filter, .
///
- private IManagedByteBuffer up;
+ private IManagedByteBuffer averageFilter;
///
- /// The buffer for the average filter
+ /// The ext buffer for the Paeth filter, .
///
- private IManagedByteBuffer average;
+ private IManagedByteBuffer paethFilter;
///
- /// The buffer for the Paeth filter
+ /// Initializes a new instance of the class.
///
- private IManagedByteBuffer paeth;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The to use for buffer allocations.
+ /// The to use for buffer allocations.
+ /// The configuration.
/// The options for influencing the encoder
- public PngEncoderCore(MemoryAllocator memoryAllocator, IPngEncoderOptions options)
+ public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoderOptions options)
{
this.memoryAllocator = memoryAllocator;
- this.pngBitDepth = options.BitDepth;
- this.pngColorType = options.ColorType;
-
- // Specification recommends default filter method None for paletted images and Paeth for others.
- this.pngFilterMethod = options.FilterMethod ?? (options.ColorType == PngColorType.Palette
- ? PngFilterMethod.None
- : PngFilterMethod.Paeth);
- this.compressionLevel = options.CompressionLevel;
- this.gamma = options.Gamma;
- this.quantizer = options.Quantizer;
- this.threshold = options.Threshold;
- this.compressTextThreshold = options.CompressTextThreshold;
+ this.configuration = configuration;
+ this.options = options;
}
///
@@ -214,98 +141,20 @@ public void Encode(Image image, Stream stream)
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
- this.configuration = image.GetConfiguration();
this.width = image.Width;
this.height = image.Height;
- // Always take the encoder options over the metadata values.
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
- this.gamma = this.gamma ?? pngMetadata.Gamma;
- this.writeGamma = this.gamma > 0;
- this.pngColorType = this.pngColorType ?? pngMetadata.ColorType;
- this.pngBitDepth = this.pngBitDepth ?? pngMetadata.BitDepth;
- this.use16Bit = this.pngBitDepth == PngBitDepth.Bit16;
-
- // Ensure we are not allowing impossible combinations.
- if (!ColorTypes.ContainsKey(this.pngColorType.Value))
- {
- throw new NotSupportedException("Color type is not supported or not valid.");
- }
+ PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
+ IQuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
+ this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
- IQuantizedFrame quantized = null;
- if (this.pngColorType == PngColorType.Palette)
- {
- byte bits = (byte)this.pngBitDepth;
- if (Array.IndexOf(ColorTypes[this.pngColorType.Value], bits) == -1)
- {
- throw new NotSupportedException("Bit depth is not supported or not valid.");
- }
-
- // Use the metadata to determine what quantization depth to use if no quantizer has been set.
- if (this.quantizer is null)
- {
- this.quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits));
- }
-
- // Create quantized frame returning the palette and set the bit depth.
- using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration()))
- {
- quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame);
- }
-
- byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
- bits = Math.Max(bits, quantizedBits);
-
- // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
- // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
- // be within the acceptable range.
- if (bits == 3)
- {
- bits = 4;
- }
- else if (bits >= 5 && bits <= 7)
- {
- bits = 8;
- }
-
- this.bitDepth = bits;
- }
- else
- {
- this.bitDepth = (byte)this.pngBitDepth;
- if (Array.IndexOf(ColorTypes[this.pngColorType.Value], this.bitDepth) == -1)
- {
- throw new NotSupportedException("Bit depth is not supported or not valid.");
- }
- }
-
- this.bytesPerPixel = this.CalculateBytesPerPixel();
-
- var header = new PngHeader(
- width: image.Width,
- height: image.Height,
- bitDepth: this.bitDepth,
- colorType: this.pngColorType.Value,
- compressionMethod: 0, // None
- filterMethod: 0,
- interlaceMethod: 0); // TODO: Can't write interlaced yet.
-
- this.WriteHeaderChunk(stream, header);
-
- // Collect the indexed pixel data
- if (quantized != null)
- {
- this.WritePaletteChunk(stream, quantized);
- }
-
- if (pngMetadata.HasTransparency)
- {
- this.WriteTransparencyChunk(stream, pngMetadata);
- }
-
+ this.WriteHeaderChunk(stream);
+ this.WritePaletteChunk(stream, quantized);
+ this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metadata);
@@ -321,27 +170,31 @@ public void Encode(Image image, Stream stream)
public void Dispose()
{
this.previousScanline?.Dispose();
- this.rawScanline?.Dispose();
- this.result?.Dispose();
- this.sub?.Dispose();
- this.up?.Dispose();
- this.average?.Dispose();
- this.paeth?.Dispose();
+ this.currentScanline?.Dispose();
+ this.subFilter?.Dispose();
+ this.averageFilter?.Dispose();
+ this.paethFilter?.Dispose();
+ this.filterBuffer?.Dispose();
+
+ this.previousScanline = null;
+ this.currentScanline = null;
+ this.subFilter = null;
+ this.averageFilter = null;
+ this.paethFilter = null;
+ this.filterBuffer = null;
}
- ///
- /// Collects a row of grayscale pixels.
- ///
+ /// Collects a row of grayscale pixels.
/// The pixel format.
/// The image row span.
private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
where TPixel : struct, IPixel
{
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- Span rawScanlineSpan = this.rawScanline.GetSpan();
+ Span rawScanlineSpan = this.currentScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
- if (this.pngColorType == PngColorType.Grayscale)
+ if (this.options.ColorType == PngColorType.Grayscale)
{
if (this.use16Bit)
{
@@ -352,7 +205,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
ref Gray16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
PixelOperations.Instance.ToGray16(this.configuration, rowSpan, luminanceSpan);
- // Can't map directly to byte array as it's big endian.
+ // Can't map directly to byte array as it's big-endian.
for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
{
Gray16 luminance = Unsafe.Add(ref luminanceRef, x);
@@ -387,7 +240,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
rowSpan,
tempSpan,
rowSpan.Length);
- this.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
}
}
}
@@ -438,7 +291,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
private void CollectTPixelBytes(ReadOnlySpan rowSpan)
where TPixel : struct, IPixel
{
- Span rawScanlineSpan = this.rawScanline.GetSpan();
+ Span rawScanlineSpan = this.currentScanline.GetSpan();
switch (this.bytesPerPixel)
{
@@ -449,7 +302,7 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan)
this.configuration,
rowSpan,
rawScanlineSpan,
- this.width);
+ rowSpan.Length);
break;
}
@@ -460,7 +313,7 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan)
this.configuration,
rowSpan,
rawScanlineSpan,
- this.width);
+ rowSpan.Length);
break;
}
@@ -519,22 +372,21 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan)
/// The row span.
/// The quantized pixels. Can be null.
/// The row.
- /// The
- private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IQuantizedFrame quantized, int row)
+ private void CollectPixelBytes(ReadOnlySpan rowSpan, IQuantizedFrame quantized, int row)
where TPixel : struct, IPixel
{
- switch (this.pngColorType)
+ switch (this.options.ColorType)
{
case PngColorType.Palette:
if (this.bitDepth < 8)
{
- this.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.rawScanline.GetSpan(), this.bitDepth);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
- int stride = this.rawScanline.Length();
- quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
+ int stride = this.currentScanline.Length();
+ quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
}
break;
@@ -546,34 +398,75 @@ private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan,
this.CollectTPixelBytes(rowSpan);
break;
}
+ }
- switch (this.pngFilterMethod)
+ ///
+ /// Apply filter for the raw scanline.
+ ///
+ private IManagedByteBuffer FilterPixelBytes()
+ {
+ switch (this.options.FilterMethod)
{
case PngFilterMethod.None:
- NoneFilter.Encode(this.rawScanline.GetSpan(), this.result.GetSpan());
- return this.result;
+ NoneFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan());
+ return this.filterBuffer;
case PngFilterMethod.Sub:
- SubFilter.Encode(this.rawScanline.GetSpan(), this.sub.GetSpan(), this.bytesPerPixel, out int _);
- return this.sub;
+ SubFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
+ return this.filterBuffer;
case PngFilterMethod.Up:
- UpFilter.Encode(this.rawScanline.GetSpan(), this.previousScanline.GetSpan(), this.up.GetSpan(), out int _);
- return this.up;
+ UpFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), out int _);
+ return this.filterBuffer;
case PngFilterMethod.Average:
- AverageFilter.Encode(this.rawScanline.GetSpan(), this.previousScanline.GetSpan(), this.average.GetSpan(), this.bytesPerPixel, out int _);
- return this.average;
+ AverageFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
+ return this.filterBuffer;
case PngFilterMethod.Paeth:
- PaethFilter.Encode(this.rawScanline.GetSpan(), this.previousScanline.GetSpan(), this.paeth.GetSpan(), this.bytesPerPixel, out int _);
- return this.paeth;
+ PaethFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
+ return this.filterBuffer;
default:
return this.GetOptimalFilteredScanline();
}
}
+ ///
+ /// Encodes the pixel data line by line.
+ /// Each scanline is encoded in the most optimal manner to improve compression.
+ ///
+ /// The pixel format.
+ /// The row span.
+ /// The quantized pixels. Can be null.
+ /// The row.
+ /// The
+ private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IQuantizedFrame quantized, int row)
+ where TPixel : struct, IPixel
+ {
+ this.CollectPixelBytes(rowSpan, quantized, row);
+ return this.FilterPixelBytes();
+ }
+
+ ///
+ /// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode.
+ ///
+ /// The row span.
+ private IManagedByteBuffer EncodeAdam7IndexedPixelRow(ReadOnlySpan rowSpan)
+ {
+ // CollectPixelBytes
+ if (this.bitDepth < 8)
+ {
+ PngEncoderHelpers.ScaleDownFrom8BitArray(rowSpan, this.currentScanline.GetSpan(), this.bitDepth);
+ }
+ else
+ {
+ rowSpan.CopyTo(this.currentScanline.GetSpan());
+ }
+
+ return this.FilterPixelBytes();
+ }
+
///
/// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed
/// to be most compressible, using lowest total variation as proxy for compressibility.
@@ -582,84 +475,67 @@ private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan,
private IManagedByteBuffer GetOptimalFilteredScanline()
{
// Palette images don't compress well with adaptive filtering.
- if (this.pngColorType == PngColorType.Palette || this.bitDepth < 8)
+ if (this.options.ColorType == PngColorType.Palette || this.bitDepth < 8)
{
- NoneFilter.Encode(this.rawScanline.GetSpan(), this.result.GetSpan());
- return this.result;
+ NoneFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan());
+ return this.filterBuffer;
}
- Span scanSpan = this.rawScanline.GetSpan();
+ this.AllocateExtBuffers();
+ Span scanSpan = this.currentScanline.GetSpan();
Span prevSpan = this.previousScanline.GetSpan();
// This order, while different to the enumerated order is more likely to produce a smaller sum
// early on which shaves a couple of milliseconds off the processing time.
- UpFilter.Encode(scanSpan, prevSpan, this.up.GetSpan(), out int currentSum);
+ UpFilter.Encode(scanSpan, prevSpan, this.filterBuffer.GetSpan(), out int currentSum);
// TODO: PERF.. We should be breaking out of the encoding for each line as soon as we hit the sum.
// That way the above comment would actually be true. It used to be anyway...
// If we could use SIMD for none branching filters we could really speed it up.
int lowestSum = currentSum;
- IManagedByteBuffer actualResult = this.up;
+ IManagedByteBuffer actualResult = this.filterBuffer;
- PaethFilter.Encode(scanSpan, prevSpan, this.paeth.GetSpan(), this.bytesPerPixel, out currentSum);
+ PaethFilter.Encode(scanSpan, prevSpan, this.paethFilter.GetSpan(), this.bytesPerPixel, out currentSum);
if (currentSum < lowestSum)
{
lowestSum = currentSum;
- actualResult = this.paeth;
+ actualResult = this.paethFilter;
}
- SubFilter.Encode(scanSpan, this.sub.GetSpan(), this.bytesPerPixel, out currentSum);
+ SubFilter.Encode(scanSpan, this.subFilter.GetSpan(), this.bytesPerPixel, out currentSum);
if (currentSum < lowestSum)
{
lowestSum = currentSum;
- actualResult = this.sub;
+ actualResult = this.subFilter;
}
- AverageFilter.Encode(scanSpan, prevSpan, this.average.GetSpan(), this.bytesPerPixel, out currentSum);
+ AverageFilter.Encode(scanSpan, prevSpan, this.averageFilter.GetSpan(), this.bytesPerPixel, out currentSum);
if (currentSum < lowestSum)
{
- actualResult = this.average;
+ actualResult = this.averageFilter;
}
return actualResult;
}
- ///
- /// Calculates the correct number of bytes per pixel for the given color type.
- ///
- /// Bytes per pixel
- private int CalculateBytesPerPixel()
- {
- switch (this.pngColorType)
- {
- case PngColorType.Grayscale:
- return this.use16Bit ? 2 : 1;
-
- case PngColorType.GrayscaleWithAlpha:
- return this.use16Bit ? 4 : 2;
-
- case PngColorType.Palette:
- return 1;
-
- case PngColorType.Rgb:
- return this.use16Bit ? 6 : 3;
-
- // PngColorType.RgbWithAlpha
- default:
- return this.use16Bit ? 8 : 4;
- }
- }
-
///
/// Writes the header chunk to the stream.
///
/// The containing image data.
- /// The .
- private void WriteHeaderChunk(Stream stream, in PngHeader header)
+ private void WriteHeaderChunk(Stream stream)
{
+ var header = new PngHeader(
+ width: this.width,
+ height: this.height,
+ bitDepth: this.bitDepth,
+ colorType: this.options.ColorType.Value,
+ compressionMethod: 0, // None
+ filterMethod: 0,
+ interlaceMethod: this.options.InterlaceMethod.Value);
+
header.WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size);
@@ -674,6 +550,11 @@ private void WriteHeaderChunk(Stream stream, in PngHeader header)
private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized)
where TPixel : struct, IPixel
{
+ if (quantized == null)
+ {
+ return;
+ }
+
// Grab the palette and write it to the stream.
ReadOnlySpan palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256);
@@ -702,7 +583,7 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
- if (alpha > this.threshold)
+ if (alpha > this.options.Threshold)
{
alpha = byte.MaxValue;
}
@@ -764,7 +645,7 @@ private void WriteTextChunks(Stream stream, PngMetadata meta)
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
- byte[] textBytes = textData.Value.Length > this.compressTextThreshold
+ byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold
? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
@@ -773,7 +654,7 @@ private void WriteTextChunks(Stream stream, PngMetadata meta)
Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5];
keywordBytes.CopyTo(outputBytes);
- if (textData.Value.Length > this.compressTextThreshold)
+ if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Indicate that the text is compressed.
outputBytes[keywordBytes.Length + 1] = 1;
@@ -788,7 +669,7 @@ private void WriteTextChunks(Stream stream, PngMetadata meta)
}
else
{
- if (textData.Value.Length > this.compressTextThreshold)
+ if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Write zTXt chunk.
byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
@@ -818,7 +699,7 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
{
using (var memoryStream = new MemoryStream())
{
- using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel))
+ using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel))
{
deflateStream.Write(textBytes);
}
@@ -833,10 +714,10 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
/// The containing image data.
private void WriteGammaChunk(Stream stream)
{
- if (this.writeGamma)
+ if (this.options.Gamma > 0)
{
// 4-byte unsigned integer of gamma * 100,000.
- uint gammaValue = (uint)(this.gamma * 100_000F);
+ uint gammaValue = (uint)(this.options.Gamma * 100_000F);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), gammaValue);
@@ -845,12 +726,17 @@ private void WriteGammaChunk(Stream stream)
}
///
- /// Writes the transparency chunk to the stream
+ /// Writes the transparency chunk to the stream.
///
/// The containing image data.
/// The image metadata.
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
+ if (!pngMetadata.HasTransparency)
+ {
+ return;
+ }
+
Span alpha = this.chunkDataBuffer.AsSpan();
if (pngMetadata.ColorType == PngColorType.Rgb)
{
@@ -899,57 +785,27 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
private void WriteDataChunks(ImageFrame pixels, IQuantizedFrame quantized, Stream stream)
where TPixel : struct, IPixel
{
- this.bytesPerScanline = this.CalculateScanlineLength(this.width);
- int resultLength = this.bytesPerScanline + 1;
-
- this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
- this.rawScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
- this.result = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
-
- switch (this.pngFilterMethod)
- {
- case PngFilterMethod.None:
- break;
-
- case PngFilterMethod.Sub:
- this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- break;
-
- case PngFilterMethod.Up:
- this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- break;
-
- case PngFilterMethod.Average:
- this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- break;
-
- case PngFilterMethod.Paeth:
- this.paeth = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- break;
-
- case PngFilterMethod.Adaptive:
- this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- this.paeth = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- break;
- }
-
byte[] buffer;
int bufferLength;
using (var memoryStream = new MemoryStream())
{
- using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel))
+ using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel))
{
- for (int y = 0; y < this.height; y++)
+ if (this.options.InterlaceMethod == PngInterlaceMode.Adam7)
{
- IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)pixels.GetPixelRowSpan(y), quantized, y);
- deflateStream.Write(r.Array, 0, resultLength);
-
- IManagedByteBuffer temp = this.rawScanline;
- this.rawScanline = this.previousScanline;
- this.previousScanline = temp;
+ if (quantized != null)
+ {
+ this.EncodeAdam7IndexedPixels(quantized, deflateStream);
+ }
+ else
+ {
+ this.EncodeAdam7Pixels(pixels, deflateStream);
+ }
+ }
+ else
+ {
+ this.EncodePixels(pixels, quantized, deflateStream);
}
}
@@ -979,6 +835,173 @@ private void WriteDataChunks(ImageFrame pixels, IQuantizedFrame<
}
}
+ ///
+ /// Allocates the buffers for each scanline.
+ ///
+ /// The bytes per scanline.
+ /// Length of the result.
+ private void AllocateBuffers(int bytesPerScanline, int resultLength)
+ {
+ // Clean up from any potential previous runs.
+ this.subFilter?.Dispose();
+ this.averageFilter?.Dispose();
+ this.paethFilter?.Dispose();
+ this.subFilter = null;
+ this.averageFilter = null;
+ this.paethFilter = null;
+
+ this.previousScanline?.Dispose();
+ this.currentScanline?.Dispose();
+ this.filterBuffer?.Dispose();
+ this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline, AllocationOptions.Clean);
+ this.currentScanline = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline, AllocationOptions.Clean);
+ this.filterBuffer = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
+ }
+
+ ///
+ /// Allocates the ext buffers for adaptive filter.
+ ///
+ private void AllocateExtBuffers()
+ {
+ if (this.subFilter == null)
+ {
+ int resultLength = this.filterBuffer.Length();
+
+ this.subFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
+ this.averageFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
+ this.paethFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
+ }
+ }
+
+ ///
+ /// Encodes the pixels.
+ ///
+ /// The type of the pixel.
+ /// The pixels.
+ /// The quantized pixels span.
+ /// The deflate stream.
+ private void EncodePixels(ImageFrame pixels, IQuantizedFrame quantized, ZlibDeflateStream deflateStream)
+ where TPixel : struct, IPixel
+ {
+ int bytesPerScanline = this.CalculateScanlineLength(this.width);
+ int resultLength = bytesPerScanline + 1;
+ this.AllocateBuffers(bytesPerScanline, resultLength);
+
+ for (int y = 0; y < this.height; y++)
+ {
+ IManagedByteBuffer r = this.EncodePixelRow(pixels.GetPixelRowSpan(y), quantized, y);
+ deflateStream.Write(r.Array, 0, resultLength);
+
+ IManagedByteBuffer temp = this.currentScanline;
+ this.currentScanline = this.previousScanline;
+ this.previousScanline = temp;
+ }
+ }
+
+ ///
+ /// Interlaced encoding the pixels.
+ ///
+ /// The type of the pixel.
+ /// The pixels.
+ /// The deflate stream.
+ private void EncodeAdam7Pixels(ImageFrame pixels, ZlibDeflateStream deflateStream)
+ where TPixel : struct, IPixel
+ {
+ int width = pixels.Width;
+ int height = pixels.Height;
+ for (int pass = 0; pass < 7; pass++)
+ {
+ int startRow = Adam7.FirstRow[pass];
+ int startCol = Adam7.FirstColumn[pass];
+ int blockWidth = Adam7.ComputeBlockWidth(width, pass);
+
+ int bytesPerScanline = this.bytesPerPixel <= 1
+ ? ((blockWidth * this.bitDepth) + 7) / 8
+ : blockWidth * this.bytesPerPixel;
+
+ int resultLength = bytesPerScanline + 1;
+
+ this.AllocateBuffers(bytesPerScanline, resultLength);
+
+ using (IMemoryOwner passData = this.memoryAllocator.Allocate(blockWidth))
+ {
+ Span destSpan = passData.Memory.Span;
+ for (int row = startRow;
+ row < height;
+ row += Adam7.RowIncrement[pass])
+ {
+ // collect data
+ Span srcRow = pixels.GetPixelRowSpan(row);
+ for (int col = startCol, i = 0;
+ col < width;
+ col += Adam7.ColumnIncrement[pass])
+ {
+ destSpan[i++] = srcRow[col];
+ }
+
+ // encode data
+ // note: quantized parameter not used
+ // note: row parameter not used
+ IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)destSpan, null, -1);
+ deflateStream.Write(r.Array, 0, resultLength);
+
+ IManagedByteBuffer temp = this.currentScanline;
+ this.currentScanline = this.previousScanline;
+ this.previousScanline = temp;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Interlaced encoding the quantized (indexed, with palette) pixels.
+ ///
+ /// The type of the pixel.
+ /// The quantized.
+ /// The deflate stream.
+ private void EncodeAdam7IndexedPixels(IQuantizedFrame quantized, ZlibDeflateStream deflateStream)
+ where TPixel : struct, IPixel
+ {
+ int width = quantized.Width;
+ int height = quantized.Height;
+ for (int pass = 0; pass < 7; pass++)
+ {
+ int startRow = Adam7.FirstRow[pass];
+ int startCol = Adam7.FirstColumn[pass];
+ int blockWidth = Adam7.ComputeBlockWidth(width, pass);
+
+ int bytesPerScanline = this.bytesPerPixel <= 1
+ ? ((blockWidth * this.bitDepth) + 7) / 8
+ : blockWidth * this.bytesPerPixel;
+
+ int resultLength = bytesPerScanline + 1;
+
+ this.AllocateBuffers(bytesPerScanline, resultLength);
+
+ using (IMemoryOwner passData = this.memoryAllocator.Allocate(blockWidth))
+ {
+ Span destSpan = passData.Memory.Span;
+ for (int row = startRow;
+ row < height;
+ row += Adam7.RowIncrement[pass])
+ {
+ // collect data
+ ReadOnlySpan srcRow = quantized.GetRowSpan(row);
+ for (int col = startCol, i = 0;
+ col < width;
+ col += Adam7.ColumnIncrement[pass])
+ {
+ destSpan[i++] = srcRow[col];
+ }
+
+ // encode data
+ IManagedByteBuffer r = this.EncodeAdam7IndexedPixelRow(destSpan);
+ deflateStream.Write(r.Array, 0, resultLength);
+ }
+ }
+ }
+ }
+
///
/// Writes the chunk end to the stream.
///
@@ -1024,48 +1047,6 @@ private void WriteChunk(Stream stream, PngChunkType type, byte[] data, int offse
stream.Write(this.buffer, 0, 4); // write the crc
}
- ///
- /// Packs the given 8 bit array into and array of depths.
- ///
- /// The source span in 8 bits.
- /// The resultant span in .
- /// The bit depth.
- /// The scaling factor.
- private void ScaleDownFrom8BitArray(ReadOnlySpan source, Span result, int bits, float scale = 1)
- {
- ref byte sourceRef = ref MemoryMarshal.GetReference(source);
- ref byte resultRef = ref MemoryMarshal.GetReference(result);
-
- int shift = 8 - bits;
- byte mask = (byte)(0xFF >> shift);
- byte shift0 = (byte)shift;
- int v = 0;
- int resultOffset = 0;
-
- for (int i = 0; i < source.Length; i++)
- {
- int value = ((int)MathF.Round(Unsafe.Add(ref sourceRef, i) / scale)) & mask;
- v |= value << shift;
-
- if (shift == 0)
- {
- shift = shift0;
- Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
- resultOffset++;
- v = 0;
- }
- else
- {
- shift -= bits;
- }
- }
-
- if (shift != shift0)
- {
- Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
- }
- }
-
///
/// Calculates the scanline length.
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoderHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderHelpers.cs
new file mode 100644
index 0000000000..78cd5d8742
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngEncoderHelpers.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// The helper methods for class.
+ ///
+ internal static class PngEncoderHelpers
+ {
+ ///
+ /// Packs the given 8 bit array into and array of depths.
+ ///
+ /// The source span in 8 bits.
+ /// The resultant span in .
+ /// The bit depth.
+ /// The scaling factor.
+ public static void ScaleDownFrom8BitArray(ReadOnlySpan source, Span result, int bits, float scale = 1)
+ {
+ ref byte sourceRef = ref MemoryMarshal.GetReference(source);
+ ref byte resultRef = ref MemoryMarshal.GetReference(result);
+
+ int shift = 8 - bits;
+ byte mask = (byte)(0xFF >> shift);
+ byte shift0 = (byte)shift;
+ int v = 0;
+ int resultOffset = 0;
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ int value = ((int)MathF.Round(Unsafe.Add(ref sourceRef, i) / scale)) & mask;
+ v |= value << shift;
+
+ if (shift == 0)
+ {
+ shift = shift0;
+ Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
+ resultOffset++;
+ v = 0;
+ }
+ else
+ {
+ shift -= bits;
+ }
+ }
+
+ if (shift != shift0)
+ {
+ Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs
new file mode 100644
index 0000000000..dd6c66cb7c
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// The options structure for the .
+ ///
+ internal class PngEncoderOptions : IPngEncoderOptions
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source.
+ public PngEncoderOptions(IPngEncoderOptions source)
+ {
+ this.BitDepth = source.BitDepth;
+ this.ColorType = source.ColorType;
+
+ // Specification recommends default filter method None for paletted images and Paeth for others.
+ this.FilterMethod = source.FilterMethod ?? (source.ColorType == PngColorType.Palette
+ ? PngFilterMethod.None
+ : PngFilterMethod.Paeth);
+ this.CompressionLevel = source.CompressionLevel;
+ this.TextCompressionThreshold = source.TextCompressionThreshold;
+ this.Gamma = source.Gamma;
+ this.Quantizer = source.Quantizer;
+ this.Threshold = source.Threshold;
+ this.InterlaceMethod = source.InterlaceMethod;
+ }
+
+ ///
+ /// Gets or sets the number of bits per sample or per palette index (not per pixel).
+ /// Not all values are allowed for all values.
+ ///
+ public PngBitDepth? BitDepth { get; set; }
+
+ ///
+ /// Gets or sets the color type.
+ ///
+ public PngColorType? ColorType { get; set; }
+
+ ///
+ /// Gets the filter method.
+ ///
+ public PngFilterMethod? FilterMethod { get; }
+
+ ///
+ /// Gets the compression level 1-9.
+ /// Defaults to 6.
+ ///
+ public int CompressionLevel { get; }
+
+ ///
+ public int TextCompressionThreshold { get; }
+
+ ///
+ /// Gets or sets the gamma value, that will be written the image.
+ ///
+ ///
+ /// The gamma value of the image.
+ ///
+ public float? Gamma { get; set; }
+
+ ///
+ /// Gets or sets the quantizer for reducing the color count.
+ ///
+ public IQuantizer Quantizer { get; set; }
+
+ ///
+ /// Gets the transparency threshold.
+ ///
+ public byte Threshold { get; }
+
+ ///
+ /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
+ ///
+ public PngInterlaceMode? InterlaceMethod { get; set; }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
new file mode 100644
index 0000000000..e3f2948864
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
@@ -0,0 +1,152 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// The helper methods for the PNG encoder options.
+ ///
+ internal static class PngEncoderOptionsHelpers
+ {
+ ///
+ /// Adjusts the options.
+ ///
+ /// The options.
+ /// The PNG metadata.
+ /// if set to true [use16 bit].
+ /// The bytes per pixel.
+ public static void AdjustOptions(
+ PngEncoderOptions options,
+ PngMetadata pngMetadata,
+ out bool use16Bit,
+ out int bytesPerPixel)
+ {
+ // Always take the encoder options over the metadata values.
+ options.Gamma = options.Gamma ?? pngMetadata.Gamma;
+ options.ColorType = options.ColorType ?? pngMetadata.ColorType;
+ options.BitDepth = options.BitDepth ?? pngMetadata.BitDepth;
+ options.InterlaceMethod = options.InterlaceMethod ?? pngMetadata.InterlaceMethod;
+
+ use16Bit = options.BitDepth == PngBitDepth.Bit16;
+ bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit);
+
+ // Ensure we are not allowing impossible combinations.
+ if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value))
+ {
+ throw new NotSupportedException("Color type is not supported or not valid.");
+ }
+ }
+
+ ///
+ /// Creates the quantized frame.
+ ///
+ /// The type of the pixel.
+ /// The options.
+ /// The image.
+ public static IQuantizedFrame CreateQuantizedFrame(
+ PngEncoderOptions options,
+ Image image)
+ where TPixel : struct, IPixel
+ {
+ if (options.ColorType != PngColorType.Palette)
+ {
+ return null;
+ }
+
+ byte bits = (byte)options.BitDepth;
+ if (Array.IndexOf(PngConstants.ColorTypes[options.ColorType.Value], bits) == -1)
+ {
+ throw new NotSupportedException("Bit depth is not supported or not valid.");
+ }
+
+ // Use the metadata to determine what quantization depth to use if no quantizer has been set.
+ if (options.Quantizer is null)
+ {
+ options.Quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits));
+ }
+
+ // Create quantized frame returning the palette and set the bit depth.
+ using (IFrameQuantizer frameQuantizer = options.Quantizer.CreateFrameQuantizer(image.GetConfiguration()))
+ {
+ return frameQuantizer.QuantizeFrame(image.Frames.RootFrame);
+ }
+ }
+
+ ///
+ /// Calculates the bit depth value.
+ ///
+ /// The type of the pixel.
+ /// The options.
+ /// The image.
+ /// The quantized frame.
+ public static byte CalculateBitDepth(
+ PngEncoderOptions options,
+ Image image,
+ IQuantizedFrame quantizedFrame)
+ where TPixel : struct, IPixel
+ {
+ byte bitDepth;
+ if (options.ColorType == PngColorType.Palette)
+ {
+ byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length).Clamp(1, 8);
+ byte bits = Math.Max((byte)options.BitDepth, quantizedBits);
+
+ // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
+ // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
+ // be within the acceptable range.
+ if (bits == 3)
+ {
+ bits = 4;
+ }
+ else if (bits >= 5 && bits <= 7)
+ {
+ bits = 8;
+ }
+
+ bitDepth = bits;
+ }
+ else
+ {
+ bitDepth = (byte)options.BitDepth;
+ }
+
+ if (Array.IndexOf(PngConstants.ColorTypes[options.ColorType.Value], bitDepth) == -1)
+ {
+ throw new NotSupportedException("Bit depth is not supported or not valid.");
+ }
+
+ return bitDepth;
+ }
+
+ ///
+ /// Calculates the correct number of bytes per pixel for the given color type.
+ ///
+ /// Bytes per pixel.
+ private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16Bit)
+ {
+ switch (pngColorType)
+ {
+ case PngColorType.Grayscale:
+ return use16Bit ? 2 : 1;
+
+ case PngColorType.GrayscaleWithAlpha:
+ return use16Bit ? 4 : 2;
+
+ case PngColorType.Palette:
+ return 1;
+
+ case PngColorType.Rgb:
+ return use16Bit ? 6 : 3;
+
+ // PngColorType.RgbWithAlpha
+ default:
+ return use16Bit ? 8 : 4;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngInterlaceMode.cs b/src/ImageSharp/Formats/Png/PngInterlaceMode.cs
index 10ebcc7bbe..e8c2db1475 100644
--- a/src/ImageSharp/Formats/Png/PngInterlaceMode.cs
+++ b/src/ImageSharp/Formats/Png/PngInterlaceMode.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Provides enumeration of available PNG interlace modes.
///
- internal enum PngInterlaceMode : byte
+ public enum PngInterlaceMode : byte
{
///
/// Non interlaced
diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs
index 8111382639..ec8779a59a 100644
--- a/src/ImageSharp/Formats/Png/PngMetaData.cs
+++ b/src/ImageSharp/Formats/Png/PngMetaData.cs
@@ -27,6 +27,7 @@ private PngMetadata(PngMetadata other)
this.BitDepth = other.BitDepth;
this.ColorType = other.ColorType;
this.Gamma = other.Gamma;
+ this.InterlaceMethod = other.InterlaceMethod;
this.HasTransparency = other.HasTransparency;
this.TransparentGray8 = other.TransparentGray8;
this.TransparentGray16 = other.TransparentGray16;
@@ -50,28 +51,37 @@ private PngMetadata(PngMetadata other)
///
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
+ ///
+ /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
+ ///
+ public PngInterlaceMode? InterlaceMethod { get; set; } = PngInterlaceMode.None;
+
///
/// Gets or sets the gamma value for the image.
///
public float Gamma { get; set; }
///
- /// Gets or sets the Rgb 24 transparent color. This represents any color in an 8 bit Rgb24 encoded png that should be transparent
+ /// Gets or sets the Rgb24 transparent color.
+ /// This represents any color in an 8 bit Rgb24 encoded png that should be transparent.
///
public Rgb24? TransparentRgb24 { get; set; }
///
- /// Gets or sets the Rgb 48 transparent color. This represents any color in a 16 bit Rgb24 encoded png that should be transparent
+ /// Gets or sets the Rgb48 transparent color.
+ /// This represents any color in a 16 bit Rgb24 encoded png that should be transparent.
///
public Rgb48? TransparentRgb48 { get; set; }
///
- /// Gets or sets the 8 bit grayscale transparent color. This represents any color in an 8 bit grayscale encoded png that should be transparent
+ /// Gets or sets the 8 bit grayscale transparent color.
+ /// This represents any color in an 8 bit grayscale encoded png that should be transparent.
///
public Gray8? TransparentGray8 { get; set; }
///
- /// Gets or sets the 16 bit grayscale transparent color. This represents any color in a 16 bit grayscale encoded png that should be transparent
+ /// Gets or sets the 16 bit grayscale transparent color.
+ /// This represents any color in a 16 bit grayscale encoded png that should be transparent.
///
public Gray16? TransparentGray16 { get; set; }
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 3f36513ef9..2584391bb7 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
// ReSharper disable InconsistentNaming
@@ -76,6 +76,12 @@ public class PngEncoderTests
80, 100, 120, 230
};
+ public static readonly PngInterlaceMode[] InterlaceMode = new[]
+ {
+ PngInterlaceMode.None,
+ PngInterlaceMode.Adam7
+ };
+
public static readonly TheoryData RatioFiles =
new TheoryData
{
@@ -99,6 +105,7 @@ public void WorksWithDifferentSizes(TestImageProvider provider,
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
+ PngInterlaceMode.None,
appendPngColorType: true);
}
@@ -107,13 +114,17 @@ public void WorksWithDifferentSizes(TestImageProvider provider,
public void IsNotBoundToSinglePixelType(TestImageProvider provider, PngColorType pngColorType)
where TPixel : struct, IPixel
{
- TestPngEncoderCore(
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
+ interlaceMode,
appendPixelType: true,
appendPngColorType: true);
+ }
}
[Theory]
@@ -121,12 +132,16 @@ public void IsNotBoundToSinglePixelType(TestImageProvider provid
public void WorksWithAllFilterMethods(TestImageProvider provider, PngFilterMethod pngFilterMethod)
where TPixel : struct, IPixel
{
- TestPngEncoderCore(
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
provider,
PngColorType.RgbWithAlpha,
pngFilterMethod,
PngBitDepth.Bit8,
+ interlaceMode,
appendPngFilterMethod: true);
+ }
}
[Theory]
@@ -134,13 +149,17 @@ public void WorksWithAllFilterMethods(TestImageProvider provider
public void WorksWithAllCompressionLevels(TestImageProvider provider, int compressionLevel)
where TPixel : struct, IPixel
{
- TestPngEncoderCore(
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
provider,
PngColorType.RgbWithAlpha,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
+ interlaceMode,
compressionLevel,
appendCompressionLevel: true);
+ }
}
[Theory]
@@ -162,14 +181,18 @@ public void WorksWithAllCompressionLevels(TestImageProvider prov
public void WorksWithAllBitDepths(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : struct, IPixel
{
- TestPngEncoderCore(
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
pngBitDepth,
+ interlaceMode,
appendPngColorType: true,
appendPixelType: true,
appendPngBitDepth: true);
+ }
}
[Theory]
@@ -177,13 +200,17 @@ public void WorksWithAllBitDepths(TestImageProvider provider, Pn
public void PaletteColorType_WuQuantizer(TestImageProvider provider, int paletteSize)
where TPixel : struct, IPixel
{
- TestPngEncoderCore(
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
provider,
PngColorType.Palette,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
+ interlaceMode,
paletteSize: paletteSize,
appendPaletteSize: true);
+ }
}
[Theory]
@@ -321,6 +348,7 @@ private static void TestPngEncoderCore(
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
+ PngInterlaceMode interlaceMode,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
@@ -339,7 +367,8 @@ private static void TestPngEncoderCore(
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
- Quantizer = new WuQuantizer(paletteSize)
+ Quantizer = new WuQuantizer(paletteSize),
+ InterlaceMethod = interlaceMode
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;
@@ -347,15 +376,16 @@ private static void TestPngEncoderCore(
string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty;
string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty;
string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
- string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}";
+ string pngInterlaceModeInfo = interlaceMode != PngInterlaceMode.None ? $"_{interlaceMode}" : string.Empty;
+
+ string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}{pngInterlaceModeInfo}";
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
// Compare to the Magick reference decoder.
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
-
// We compare using both our decoder and the reference decoder as pixel transformation
- // occurrs within the encoder itself leaving the input image unaffected.
+ // occurs within the encoder itself leaving the input image unaffected.
// This means we are benefiting from testing our decoder also.
using (var imageSharpImage = Image.Load(actualOutputFile, new PngDecoder()))
using (var referenceImage = Image.Load(actualOutputFile, referenceDecoder))
@@ -365,4 +395,4 @@ private static void TestPngEncoderCore(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs
index db4d7d69d4..33fd8ead21 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs
@@ -28,6 +28,7 @@ public void CloneIsDeep()
{
BitDepth = PngBitDepth.Bit16,
ColorType = PngColorType.GrayscaleWithAlpha,
+ InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List() { new PngTextData("name", "value", "foo", "bar") }
};
@@ -36,10 +37,12 @@ public void CloneIsDeep()
clone.BitDepth = PngBitDepth.Bit2;
clone.ColorType = PngColorType.Palette;
+ clone.InterlaceMethod = PngInterlaceMode.None;
clone.Gamma = 1;
- Assert.False(meta.BitDepth.Equals(clone.BitDepth));
- Assert.False(meta.ColorType.Equals(clone.ColorType));
+ Assert.False(meta.BitDepth == clone.BitDepth);
+ Assert.False(meta.ColorType == clone.ColorType);
+ Assert.False(meta.InterlaceMethod == clone.InterlaceMethod);
Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
@@ -132,7 +135,7 @@ public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(T
inputMetadata.TextData.Add(expectedTextNoneLatin);
input.Save(memoryStream, new PngEncoder()
{
- CompressTextThreshold = 50
+ TextCompressionThreshold = 50
});
memoryStream.Position = 0;