Browse Source

#244 Add support for interlaced PNG encoding (#955)

* #244 Implement interlaced PNG encoding

* #244 Update documentations

* #244 Remove comment

* Cleanup

* Update PngEncoderCore.cs
af/merge-core
Ildar 7 years ago
committed by James Jackson-South
parent
commit
5aec0d8fe9
  1. 32
      src/ImageSharp/Formats/Png/Adam7.cs
  2. 7
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  3. 2
      src/ImageSharp/Formats/Png/PngConstants.cs
  4. 29
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  5. 16
      src/ImageSharp/Formats/Png/PngEncoder.cs
  6. 683
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  7. 57
      src/ImageSharp/Formats/Png/PngEncoderHelpers.cs
  8. 82
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  9. 152
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  10. 2
      src/ImageSharp/Formats/Png/PngInterlaceMode.cs
  11. 18
      src/ImageSharp/Formats/Png/PngMetaData.cs
  12. 52
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  13. 9
      tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs

32
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 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
/// <summary>
/// Gets the width of the block.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="pass">The pass.</param>
/// <returns>
/// The <see cref="int" />
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeBlockWidth(int width, int pass)
{
return (width + ColumnIncrement[pass] - 1 - FirstColumn[pass]) / ColumnIncrement[pass];
}
/// <summary>
/// Gets the height of the block.
/// </summary>
/// <param name="height">The height.</param>
/// <param name="pass">The pass.</param>
/// <returns>
/// The <see cref="int" />
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeBlockHeight(int height, int pass)
{
return (height + RowIncrement[pass] - 1 - FirstRow[pass]) / RowIncrement[pass];
}
/// <summary>
/// Returns the correct number of columns for each interlaced pass.
/// </summary>
@ -53,4 +81,4 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
}
}
}

7
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets the threshold of characters in text metadata, when compression should be used.
/// </summary>
int CompressTextThreshold { get; }
int TextCompressionThreshold { get; }
/// <summary>
/// Gets the gamma value, that will be written the image.
@ -52,5 +52,10 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets the transparency threshold.
/// </summary>
byte Threshold { get; }
/// <summary>
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
PngInterlaceMode? InterlaceMethod { get; }
}
}

2
src/ImageSharp/Formats/Png/PngConstants.cs

@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Png
};
/// <summary>
/// The header bytes as a big endian coded ulong.
/// The header bytes as a big-endian coded ulong.
/// </summary>
public const ulong HeaderValue = 0x89504E470D0A1A0AUL;

29
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly byte[] buffer = new byte[4];
/// <summary>
/// Reusable crc for validating chunks.
/// Reusable CRC for validating chunks.
/// </summary>
private readonly Crc32 crc = new Crc32();
@ -106,12 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private int currentRow = Adam7.FirstRow[0];
/// <summary>
/// The current pass for an interlaced PNG.
/// </summary>
private int pass;
/// <summary>
/// The current number of bytes read in the current scanline.
/// The current number of bytes read in the current scanline
/// </summary>
private int currentRowBytesRead;
@ -551,13 +546,15 @@ namespace SixLabors.ImageSharp.Formats.Png
private void DecodeInterlacedPixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata)
where TPixel : struct, IPixel<TPixel>
{
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 @@ namespace SixLabors.ImageSharp.Formats.Png
}
Span<TPixel> 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 @@ namespace SixLabors.ImageSharp.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Formats.Png
}
result = default;
return false;
}

16
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -36,9 +36,10 @@ namespace SixLabors.ImageSharp.Formats.Png
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// 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.
/// </summary>
public int CompressTextThreshold { get; set; } = 1024;
public int TextCompressionThreshold { get; set; } = 1024;
/// <summary>
/// Gets or sets the gamma value, that will be written the image.
@ -47,14 +48,19 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets or sets quantizer for reducing the color count.
/// Defaults to the <see cref="WuQuantizer"/>
/// Defaults to the <see cref="WuQuantizer"/>.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 255;
public byte Threshold { get; set; } = byte.MaxValue;
/// <summary>
/// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
@ -65,7 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), this))
using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this)))
{
encoder.Encode(image, stream);
}

683
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
{
/// <summary>
/// The dictionary of available color types.
/// The maximum block size, defaults at 64k for uncompressed blocks.
/// </summary>
private static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>()
{
[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;
/// <summary>
/// Used the manage memory allocations.
@ -48,12 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// The configuration instance for the decoding operation.
/// </summary>
private Configuration configuration;
/// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks.
/// </summary>
private const int MaxBlockSize = 65535;
private readonly Configuration configuration;
/// <summary>
/// Reusable buffer for writing general data.
@ -66,44 +52,19 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly byte[] chunkDataBuffer = new byte[16];
/// <summary>
/// Reusable crc for validating chunks.
/// Reusable CRC for validating chunks.
/// </summary>
private readonly Crc32 crc = new Crc32();
/// <summary>
/// The png filter method.
/// </summary>
private readonly PngFilterMethod pngFilterMethod;
/// <summary>
/// Gets or sets the CompressionLevel value.
/// </summary>
private readonly int compressionLevel;
/// <summary>
/// The threshold of characters in text metadata, when compression should be used.
/// </summary>
private readonly int compressTextThreshold;
/// <summary>
/// Gets or sets the alpha threshold value
/// The encoder options
/// </summary>
private readonly byte threshold;
private readonly PngEncoderOptions options;
/// <summary>
/// The quantizer for reducing the color count.
/// The bit depth.
/// </summary>
private IQuantizer quantizer;
/// <summary>
/// Gets or sets a value indicating whether to write the gamma chunk.
/// </summary>
private bool writeGamma;
/// <summary>
/// The png bit depth.
/// </summary>
private PngBitDepth? pngBitDepth;
private byte bitDepth;
/// <summary>
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
@ -111,14 +72,9 @@ namespace SixLabors.ImageSharp.Formats.Png
private bool use16Bit;
/// <summary>
/// The png color type.
/// </summary>
private PngColorType? pngColorType;
/// <summary>
/// Gets or sets the Gamma value
/// The number of bytes per pixel.
/// </summary>
private float? gamma;
private int bytesPerPixel;
/// <summary>
/// The image width.
@ -131,75 +87,46 @@ namespace SixLabors.ImageSharp.Formats.Png
private int height;
/// <summary>
/// The number of bits required to encode the colors in the png.
/// </summary>
private byte bitDepth;
/// <summary>
/// The number of bytes per pixel.
/// </summary>
private int bytesPerPixel;
/// <summary>
/// The number of bytes per scanline.
/// </summary>
private int bytesPerScanline;
/// <summary>
/// The previous scanline.
/// The raw data of previous scanline.
/// </summary>
private IManagedByteBuffer previousScanline;
/// <summary>
/// The raw scanline.
/// The raw data of current scanline.
/// </summary>
private IManagedByteBuffer rawScanline;
private IManagedByteBuffer currentScanline;
/// <summary>
/// The filtered scanline result.
/// The common buffer for the filters.
/// </summary>
private IManagedByteBuffer result;
private IManagedByteBuffer filterBuffer;
/// <summary>
/// The buffer for the sub filter
/// The ext buffer for the sub filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer sub;
private IManagedByteBuffer subFilter;
/// <summary>
/// The buffer for the up filter
/// The ext buffer for the average filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer up;
private IManagedByteBuffer averageFilter;
/// <summary>
/// The buffer for the average filter
/// The ext buffer for the Paeth filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer average;
private IManagedByteBuffer paethFilter;
/// <summary>
/// The buffer for the Paeth filter
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary>
private IManagedByteBuffer paeth;
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore"/> class.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="options">The options for influencing the encoder</param>
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;
}
/// <summary>
@ -214,98 +141,20 @@ namespace SixLabors.ImageSharp.Formats.Png
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<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
IQuantizedFrame<TPixel> 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<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(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 @@ namespace SixLabors.ImageSharp.Formats.Png
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;
}
/// <summary>
/// Collects a row of grayscale pixels.
/// </summary>
/// <summary>Collects a row of grayscale pixels.</summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The image row span.</param>
private void CollectGrayscaleBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : struct, IPixel<TPixel>
{
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> rawScanlineSpan = this.rawScanline.GetSpan();
Span<byte> 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 @@ namespace SixLabors.ImageSharp.Formats.Png
ref Gray16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
PixelOperations<TPixel>.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 @@ namespace SixLabors.ImageSharp.Formats.Png
rowSpan,
tempSpan,
rowSpan.Length);
this.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
}
}
}
@ -438,7 +291,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private void CollectTPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : struct, IPixel<TPixel>
{
Span<byte> rawScanlineSpan = this.rawScanline.GetSpan();
Span<byte> rawScanlineSpan = this.currentScanline.GetSpan();
switch (this.bytesPerPixel)
{
@ -449,7 +302,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
rowSpan.Length);
break;
}
@ -460,7 +313,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
rowSpan.Length);
break;
}
@ -519,22 +372,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IQuantizedFrame<TPixel> quantized, int row)
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IQuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
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 @@ namespace SixLabors.ImageSharp.Formats.Png
this.CollectTPixelBytes(rowSpan);
break;
}
}
switch (this.pngFilterMethod)
/// <summary>
/// Apply filter for the raw scanline.
/// </summary>
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();
}
}
/// <summary>
/// Encodes the pixel data line by line.
/// Each scanline is encoded in the most optimal manner to improve compression.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IQuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
this.CollectPixelBytes(rowSpan, quantized, row);
return this.FilterPixelBytes();
}
/// <summary>
/// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode.
/// </summary>
/// <param name="rowSpan">The row span.</param>
private IManagedByteBuffer EncodeAdam7IndexedPixelRow(ReadOnlySpan<byte> rowSpan)
{
// CollectPixelBytes
if (this.bitDepth < 8)
{
PngEncoderHelpers.ScaleDownFrom8BitArray(rowSpan, this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
rowSpan.CopyTo(this.currentScanline.GetSpan());
}
return this.FilterPixelBytes();
}
/// <summary>
/// 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 @@ namespace SixLabors.ImageSharp.Formats.Png
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<byte> scanSpan = this.rawScanline.GetSpan();
this.AllocateExtBuffers();
Span<byte> scanSpan = this.currentScanline.GetSpan();
Span<byte> 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;
}
/// <summary>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
/// <returns>Bytes per pixel</returns>
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;
}
}
/// <summary>
/// Writes the header chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
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 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WritePaletteChunk<TPixel>(Stream stream, IQuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
if (quantized == null)
{
return;
}
// Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256);
@ -702,7 +583,7 @@ namespace SixLabors.ImageSharp.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Formats.Png
{
// 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 @@ namespace SixLabors.ImageSharp.Formats.Png
Span<byte> 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 @@ namespace SixLabors.ImageSharp.Formats.Png
}
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 @@ namespace SixLabors.ImageSharp.Formats.Png
{
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 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
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 @@ namespace SixLabors.ImageSharp.Formats.Png
}
/// <summary>
/// Writes the transparency chunk to the stream
/// Writes the transparency chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="pngMetadata">The image metadata.</param>
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
if (!pngMetadata.HasTransparency)
{
return;
}
Span<byte> alpha = this.chunkDataBuffer.AsSpan();
if (pngMetadata.ColorType == PngColorType.Rgb)
{
@ -899,57 +785,27 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IQuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel>)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 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
/// <summary>
/// Allocates the buffers for each scanline.
/// </summary>
/// <param name="bytesPerScanline">The bytes per scanline.</param>
/// <param name="resultLength">Length of the result.</param>
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);
}
/// <summary>
/// Allocates the ext buffers for adaptive filter.
/// </summary>
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);
}
}
/// <summary>
/// Encodes the pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IQuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : struct, IPixel<TPixel>
{
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;
}
}
/// <summary>
/// Interlaced encoding the pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="pixels">The pixels.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> pixels, ZlibDeflateStream deflateStream)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel> passData = this.memoryAllocator.Allocate<TPixel>(blockWidth))
{
Span<TPixel> destSpan = passData.Memory.Span;
for (int row = startRow;
row < height;
row += Adam7.RowIncrement[pass])
{
// collect data
Span<TPixel> 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<TPixel>)destSpan, null, -1);
deflateStream.Write(r.Array, 0, resultLength);
IManagedByteBuffer temp = this.currentScanline;
this.currentScanline = this.previousScanline;
this.previousScanline = temp;
}
}
}
}
/// <summary>
/// Interlaced encoding the quantized (indexed, with palette) pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(IQuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : struct, IPixel<TPixel>
{
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<byte> passData = this.memoryAllocator.Allocate<byte>(blockWidth))
{
Span<byte> destSpan = passData.Memory.Span;
for (int row = startRow;
row < height;
row += Adam7.RowIncrement[pass])
{
// collect data
ReadOnlySpan<byte> 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);
}
}
}
}
/// <summary>
/// Writes the chunk end to the stream.
/// </summary>
@ -1024,48 +1047,6 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(this.buffer, 0, 4); // write the crc
}
/// <summary>
/// Packs the given 8 bit array into and array of <paramref name="bits"/> depths.
/// </summary>
/// <param name="source">The source span in 8 bits.</param>
/// <param name="result">The resultant span in <paramref name="bits"/>.</param>
/// <param name="bits">The bit depth.</param>
/// <param name="scale">The scaling factor.</param>
private void ScaleDownFrom8BitArray(ReadOnlySpan<byte> source, Span<byte> 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;
}
}
/// <summary>
/// Calculates the scanline length.
/// </summary>

57
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
{
/// <summary>
/// The helper methods for <see cref="PngEncoderCore"/> class.
/// </summary>
internal static class PngEncoderHelpers
{
/// <summary>
/// Packs the given 8 bit array into and array of <paramref name="bits"/> depths.
/// </summary>
/// <param name="source">The source span in 8 bits.</param>
/// <param name="result">The resultant span in <paramref name="bits"/>.</param>
/// <param name="bits">The bit depth.</param>
/// <param name="scale">The scaling factor.</param>
public static void ScaleDownFrom8BitArray(ReadOnlySpan<byte> source, Span<byte> 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;
}
}
}
}

82
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
{
/// <summary>
/// The options structure for the <see cref="PngEncoderCore"/>.
/// </summary>
internal class PngEncoderOptions : IPngEncoderOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderOptions"/> class.
/// </summary>
/// <param name="source">The source.</param>
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;
}
/// <summary>
/// Gets or sets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="P:SixLabors.ImageSharp.Formats.Png.IPngEncoderOptions.ColorType" /> values.
/// </summary>
public PngBitDepth? BitDepth { get; set; }
/// <summary>
/// Gets or sets the color type.
/// </summary>
public PngColorType? ColorType { get; set; }
/// <summary>
/// Gets the filter method.
/// </summary>
public PngFilterMethod? FilterMethod { get; }
/// <summary>
/// Gets the compression level 1-9.
/// <remarks>Defaults to 6.</remarks>
/// </summary>
public int CompressionLevel { get; }
/// <inheritdoc/>
public int TextCompressionThreshold { get; }
/// <summary>
/// Gets or sets the gamma value, that will be written the image.
/// </summary>
/// <value>
/// The gamma value of the image.
/// </value>
public float? Gamma { get; set; }
/// <summary>
/// Gets or sets the quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets the transparency threshold.
/// </summary>
public byte Threshold { get; }
/// <summary>
/// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; set; }
}
}

152
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
{
/// <summary>
/// The helper methods for the PNG encoder options.
/// </summary>
internal static class PngEncoderOptionsHelpers
{
/// <summary>
/// Adjusts the options.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="pngMetadata">The PNG metadata.</param>
/// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param>
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.");
}
}
/// <summary>
/// Creates the quantized frame.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
public static IQuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options,
Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel> frameQuantizer = options.Quantizer.CreateFrameQuantizer<TPixel>(image.GetConfiguration()))
{
return frameQuantizer.QuantizeFrame(image.Frames.RootFrame);
}
}
/// <summary>
/// Calculates the bit depth value.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
/// <param name="quantizedFrame">The quantized frame.</param>
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
IQuantizedFrame<TPixel> quantizedFrame)
where TPixel : struct, IPixel<TPixel>
{
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;
}
/// <summary>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
/// <returns>Bytes per pixel.</returns>
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;
}
}
}
}

2
src/ImageSharp/Formats/Png/PngInterlaceMode.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Provides enumeration of available PNG interlace modes.
/// </summary>
internal enum PngInterlaceMode : byte
public enum PngInterlaceMode : byte
{
/// <summary>
/// Non interlaced

18
src/ImageSharp/Formats/Png/PngMetaData.cs

@ -27,6 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
/// <summary>
/// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; set; } = PngInterlaceMode.None;
/// <summary>
/// Gets or sets the gamma value for the image.
/// </summary>
public float Gamma { get; set; }
/// <summary>
/// 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.
/// </summary>
public Rgb24? TransparentRgb24 { get; set; }
/// <summary>
/// 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.
/// </summary>
public Rgb48? TransparentRgb48 { get; set; }
/// <summary>
/// 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.
/// </summary>
public Gray8? TransparentGray8 { get; set; }
/// <summary>
/// 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.
/// </summary>
public Gray16? TransparentGray16 { get; set; }

52
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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
80, 100, 120, 230
};
public static readonly PngInterlaceMode[] InterlaceMode = new[]
{
PngInterlaceMode.None,
PngInterlaceMode.Adam7
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
@ -99,6 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
PngInterlaceMode.None,
appendPngColorType: true);
}
@ -107,13 +114,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public void IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
appendPixelType: true,
appendPngColorType: true);
}
}
[Theory]
@ -121,12 +132,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public void WorksWithAllFilterMethods<TPixel>(TestImageProvider<TPixel> provider, PngFilterMethod pngFilterMethod)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
PngColorType.RgbWithAlpha,
pngFilterMethod,
PngBitDepth.Bit8,
interlaceMode,
appendPngFilterMethod: true);
}
}
[Theory]
@ -134,13 +149,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public void WorksWithAllCompressionLevels<TPixel>(TestImageProvider<TPixel> provider, int compressionLevel)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
PngColorType.RgbWithAlpha,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
compressionLevel,
appendCompressionLevel: true);
}
}
[Theory]
@ -162,14 +181,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public void WorksWithAllBitDepths<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
pngBitDepth,
interlaceMode,
appendPngColorType: true,
appendPixelType: true,
appendPngBitDepth: true);
}
}
[Theory]
@ -177,13 +200,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public void PaletteColorType_WuQuantizer<TPixel>(TestImageProvider<TPixel> provider, int paletteSize)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
PngColorType.Palette,
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
paletteSize: paletteSize,
appendPaletteSize: true);
}
}
[Theory]
@ -321,6 +348,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
PngInterlaceMode interlaceMode,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
@ -339,7 +367,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
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<TPixel>(actualOutputFile, new PngDecoder()))
using (var referenceImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
@ -365,4 +395,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
}
}
}

9
tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs

@ -28,6 +28,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
BitDepth = PngBitDepth.Bit16,
ColorType = PngColorType.GrayscaleWithAlpha,
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List<PngTextData>() { new PngTextData("name", "value", "foo", "bar") }
};
@ -36,10 +37,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
inputMetadata.TextData.Add(expectedTextNoneLatin);
input.Save(memoryStream, new PngEncoder()
{
CompressTextThreshold = 50
TextCompressionThreshold = 50
});
memoryStream.Position = 0;

Loading…
Cancel
Save