//
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
//
namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Quantizers;
using static ComparableExtensions;
///
/// Performs the png encoding operation.
///
internal sealed class PngEncoderCore
{
///
/// The maximum block size, defaults at 64k for uncompressed blocks.
///
private const int MaxBlockSize = 65535;
///
/// Reusable buffer for writing chunk types.
///
private readonly byte[] chunkTypeBuffer = new byte[4];
///
/// Reusable buffer for writing chunk data.
///
private readonly byte[] chunkDataBuffer = new byte[16];
///
/// Reusable crc for validating chunks.
///
private readonly Crc32 crc = new Crc32();
///
/// Contains the raw pixel data from an indexed image.
///
private byte[] palettePixelData;
///
/// The image width.
///
private int width;
///
/// The image height.
///
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 buffer for the sub filter
///
private byte[] sub;
///
/// The buffer for the up filter
///
private byte[] up;
///
/// The buffer for the average filter
///
private byte[] average;
///
/// The buffer for the paeth filter
///
private byte[] paeth;
///
/// Gets or sets the quality of output for images.
///
public int Quality { get; set; }
///
/// Gets or sets the png color type
///
public PngColorType PngColorType { get; set; }
///
/// Gets or sets the compression level 1-9.
/// Defaults to 6.
///
public int CompressionLevel { get; set; } = 6;
///
/// Gets or sets a value indicating whether this instance should write
/// gamma information to the stream. The default value is false.
///
public bool WriteGamma { get; set; }
///
/// Gets or sets the gamma value, that will be written
/// the the stream, when the property
/// is set to true. The default value is 2.2F.
///
/// The gamma value of the image.
public float Gamma { get; set; } = 2.2F;
///
/// Gets or sets the quantizer for reducing the color count.
///
public IQuantizer Quantizer { get; set; }
///
/// Gets or sets the transparency threshold.
///
public byte Threshold { get; set; }
///
/// Encodes the image to the specified stream from the .
///
/// The pixel format.
/// The to encode from.
/// The to encode the image data to.
public void Encode(Image image, Stream stream)
where TColor : struct, IPackedPixel, IEquatable
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.width = image.Width;
this.height = image.Height;
// Write the png header.
this.chunkDataBuffer[0] = 0x89; // Set the high bit.
this.chunkDataBuffer[1] = 0x50; // P
this.chunkDataBuffer[2] = 0x4E; // N
this.chunkDataBuffer[3] = 0x47; // G
this.chunkDataBuffer[4] = 0x0D; // Line ending CRLF
this.chunkDataBuffer[5] = 0x0A; // Line ending CRLF
this.chunkDataBuffer[6] = 0x1A; // EOF
this.chunkDataBuffer[7] = 0x0A; // LF
stream.Write(this.chunkDataBuffer, 0, 8);
// Ensure that quality can be set but has a fallback.
int quality = this.Quality > 0 ? this.Quality : image.MetaData.Quality;
this.Quality = quality > 0 ? quality.Clamp(1, int.MaxValue) : int.MaxValue;
// Set correct color type if the color count is 256 or less.
if (this.Quality <= 256)
{
this.PngColorType = PngColorType.Palette;
}
if (this.PngColorType == PngColorType.Palette && this.Quality > 256)
{
this.Quality = 256;
}
// Set correct bit depth.
this.bitDepth = this.Quality <= 256
? (byte)ImageMaths.GetBitsNeededForColorDepth(this.Quality).Clamp(1, 8)
: (byte)8;
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
if (this.bitDepth == 3)
{
this.bitDepth = 4;
}
else if (this.bitDepth >= 5 || this.bitDepth <= 7)
{
this.bitDepth = 8;
}
this.bytesPerPixel = this.CalculateBytesPerPixel();
PngHeader header = new PngHeader
{
Width = image.Width,
Height = image.Height,
ColorType = (byte)this.PngColorType,
BitDepth = this.bitDepth,
FilterMethod = 0, // None
CompressionMethod = 0,
InterlaceMethod = 0
};
this.WriteHeaderChunk(stream, header);
// Collect the indexed pixel data
if (this.PngColorType == PngColorType.Palette)
{
this.CollectIndexedBytes(image, stream, header);
}
this.WritePhysicalChunk(stream, image);
this.WriteGammaChunk(stream);
using (PixelAccessor pixels = image.Lock())
{
this.WriteDataChunks(pixels, stream);
}
this.WriteEndChunk(stream);
stream.Flush();
}
///
/// Writes an integer to the byte array.
///
/// The containing image data.
/// The amount to offset by.
/// The value to write.
private static void WriteInteger(byte[] data, int offset, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
buffer.ReverseBytes();
Buffer.BlockCopy(buffer, 0, data, offset, 4);
}
///
/// Writes an integer to the stream.
///
/// The containing image data.
/// The value to write.
private static void WriteInteger(Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
buffer.ReverseBytes();
stream.Write(buffer, 0, 4);
}
///
/// Writes an unsigned integer to the stream.
///
/// The containing image data.
/// The value to write.
private static void WriteInteger(Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
buffer.ReverseBytes();
stream.Write(buffer, 0, 4);
}
///
/// Collects the indexed pixel data.
///
/// The pixel format.
/// The image to encode.
/// The containing image data.
/// The .
private void CollectIndexedBytes(ImageBase image, Stream stream, PngHeader header)
where TColor : struct, IPackedPixel, IEquatable
{
// Quantize the image and get the pixels.
QuantizedImage quantized = this.WritePaletteChunk(stream, header, image);
this.palettePixelData = quantized.Pixels;
}
///
/// Collects a row of grayscale pixels.
///
/// The pixel format.
/// The image pixels accessor.
/// The row index.
/// The raw scanline.
private void CollectGrayscaleBytes(PixelAccessor pixels, int row, byte[] rawScanline)
where TColor : struct, IPackedPixel, IEquatable
{
// Copy the pixels across from the image.
// Reuse the chunk type buffer.
for (int x = 0; x < this.width; x++)
{
// Convert the color to YCbCr and store the luminance
// Optionally store the original color alpha.
int offset = x * this.bytesPerPixel;
pixels[x, row].ToXyzwBytes(this.chunkTypeBuffer, 0);
byte luminance = (byte)((0.299F * this.chunkTypeBuffer[0]) + (0.587F * this.chunkTypeBuffer[1]) + (0.114F * this.chunkTypeBuffer[2]));
for (int i = 0; i < this.bytesPerPixel; i++)
{
if (i == 0)
{
rawScanline[offset] = luminance;
}
else
{
rawScanline[offset + i] = this.chunkTypeBuffer[3];
}
}
}
}
///
/// Collects a row of true color pixel data.
///
/// The pixel format.
/// The image pixel accessor.
/// The row index.
/// The raw scanline.
private void CollectColorBytes(PixelAccessor pixels, int row, byte[] rawScanline)
where TColor : struct, IPackedPixel, IEquatable
{
// We can use the optimized PixelAccessor here and copy the bytes in unmanaged memory.
using (PixelArea pixelRow = new PixelArea(this.width, rawScanline, this.bytesPerPixel == 4 ? ComponentOrder.Xyzw : ComponentOrder.Xyz))
{
pixels.CopyTo(pixelRow, row);
}
}
///
/// Encodes the pixel data line by line.
/// Each scanline is encoded in the most optimal manner to improve compression.
///
/// The pixel format.
/// The image pixel accessor.
/// The row.
/// The previous scanline.
/// The raw scanline.
/// The filtered scanline result.
/// The
private byte[] EncodePixelRow(PixelAccessor pixels, int row, byte[] previousScanline, byte[] rawScanline, byte[] result)
where TColor : struct, IPackedPixel, IEquatable
{
switch (this.PngColorType)
{
case PngColorType.Palette:
Buffer.BlockCopy(this.palettePixelData, row * rawScanline.Length, rawScanline, 0, rawScanline.Length);
break;
case PngColorType.Grayscale:
case PngColorType.GrayscaleWithAlpha:
this.CollectGrayscaleBytes(pixels, row, rawScanline);
break;
default:
this.CollectColorBytes(pixels, row, rawScanline);
break;
}
return this.GetOptimalFilteredScanline(rawScanline, previousScanline, result);
}
///
/// 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.
///
/// The raw scanline
/// The previous scanline
/// The filtered scanline result.
/// The
private byte[] GetOptimalFilteredScanline(byte[] rawScanline, byte[] previousScanline, byte[] result)
{
// Palette images don't compress well with adaptive filtering.
if (this.PngColorType == PngColorType.Palette || this.bitDepth < 8)
{
NoneFilter.Encode(rawScanline, result);
return result;
}
// 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(rawScanline, previousScanline, this.up);
int currentSum = this.CalculateTotalVariation(this.up, int.MaxValue);
int lowestSum = currentSum;
result = this.up;
PaethFilter.Encode(rawScanline, previousScanline, this.paeth, this.bytesPerPixel);
currentSum = this.CalculateTotalVariation(this.paeth, currentSum);
if (currentSum < lowestSum)
{
lowestSum = currentSum;
result = this.paeth;
}
SubFilter.Encode(rawScanline, this.sub, this.bytesPerPixel);
currentSum = this.CalculateTotalVariation(this.sub, int.MaxValue);
if (currentSum < lowestSum)
{
lowestSum = currentSum;
result = this.sub;
}
AverageFilter.Encode(rawScanline, previousScanline, this.average, this.bytesPerPixel);
currentSum = this.CalculateTotalVariation(this.average, currentSum);
if (currentSum < lowestSum)
{
result = this.average;
}
return result;
}
///
/// Calculates the total variation of given byte array. Total variation is the sum of the absolute values of
/// neighbor differences.
///
/// The scanline bytes
/// The last variation sum
/// The
private int CalculateTotalVariation(byte[] scanline, int lastSum)
{
int sum = 0;
for (int i = 1; i < scanline.Length; i++)
{
byte v = scanline[i];
sum += v < 128 ? v : 256 - v;
// No point continuing if we are larger.
if (sum > lastSum)
{
break;
}
}
return sum;
}
///
/// Calculates the correct number of bytes per pixel for the given color type.
///
/// The
private int CalculateBytesPerPixel()
{
switch (this.PngColorType)
{
case PngColorType.Grayscale:
return 1;
case PngColorType.GrayscaleWithAlpha:
return 2;
case PngColorType.Palette:
return 1;
case PngColorType.Rgb:
return 3;
// PngColorType.RgbWithAlpha
// TODO: Maybe figure out a way to detect if there are any transparent
// pixels and encode RGB if none.
default:
return 4;
}
}
///
/// Writes the header chunk to the stream.
///
/// The containing image data.
/// The .
private void WriteHeaderChunk(Stream stream, PngHeader header)
{
WriteInteger(this.chunkDataBuffer, 0, header.Width);
WriteInteger(this.chunkDataBuffer, 4, header.Height);
this.chunkDataBuffer[8] = header.BitDepth;
this.chunkDataBuffer[9] = header.ColorType;
this.chunkDataBuffer[10] = header.CompressionMethod;
this.chunkDataBuffer[11] = header.FilterMethod;
this.chunkDataBuffer[12] = (byte)header.InterlaceMethod;
this.WriteChunk(stream, PngChunkTypes.Header, this.chunkDataBuffer, 0, 13);
}
///
/// Writes the palette chunk to the stream.
///
/// The pixel format.
/// The containing image data.
/// The .
/// The image to encode.
/// The
private QuantizedImage WritePaletteChunk(Stream stream, PngHeader header, ImageBase image)
where TColor : struct, IPackedPixel, IEquatable
{
if (this.Quality > 256)
{
return null;
}
if (this.Quantizer == null)
{
this.Quantizer = new WuQuantizer();
}
// Quantize the image returning a palette. This boxing is icky.
QuantizedImage quantized = ((IQuantizer)this.Quantizer).Quantize(image, this.Quality);
// Grab the palette and write it to the stream.
TColor[] palette = quantized.Palette;
int pixelCount = palette.Length;
List transparentPixels = new List();
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3;
byte[] colorTable = ArrayPool.Shared.Rent(colorTableLength);
byte[] bytes = ArrayPool.Shared.Rent(4);
try
{
for (int i = 0; i < pixelCount; i++)
{
int offset = i * 3;
palette[i].ToXyzwBytes(bytes, 0);
int alpha = bytes[3];
colorTable[offset] = bytes[0];
colorTable[offset + 1] = bytes[1];
colorTable[offset + 2] = bytes[2];
if (alpha <= this.Threshold)
{
transparentPixels.Add((byte)offset);
}
}
this.WriteChunk(stream, PngChunkTypes.Palette, colorTable, 0, colorTableLength);
}
finally
{
ArrayPool.Shared.Return(colorTable);
ArrayPool.Shared.Return(bytes);
}
// Write the transparency data
if (transparentPixels.Any())
{
this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, transparentPixels.ToArray());
}
return quantized;
}
///
/// Writes the physical dimension information to the stream.
///
/// The pixel format.
/// The containing image data.
/// The image base.
private void WritePhysicalChunk(Stream stream, ImageBase imageBase)
where TColor : struct, IPackedPixel, IEquatable
{
Image image = imageBase as Image;
if (image != null && image.MetaData.HorizontalResolution > 0 && image.MetaData.VerticalResolution > 0)
{
// 39.3700787 = inches in a meter.
int dpmX = (int)Math.Round(image.MetaData.HorizontalResolution * 39.3700787D);
int dpmY = (int)Math.Round(image.MetaData.VerticalResolution * 39.3700787D);
WriteInteger(this.chunkDataBuffer, 0, dpmX);
WriteInteger(this.chunkDataBuffer, 4, dpmY);
this.chunkDataBuffer[8] = 1;
this.WriteChunk(stream, PngChunkTypes.Physical, this.chunkDataBuffer, 0, 9);
}
}
///
/// Writes the gamma information to the stream.
///
/// The containing image data.
private void WriteGammaChunk(Stream stream)
{
if (this.WriteGamma)
{
int gammaValue = (int)(this.Gamma * 100000F);
byte[] size = BitConverter.GetBytes(gammaValue);
this.chunkDataBuffer[0] = size[3];
this.chunkDataBuffer[1] = size[2];
this.chunkDataBuffer[2] = size[1];
this.chunkDataBuffer[3] = size[0];
this.WriteChunk(stream, PngChunkTypes.Gamma, this.chunkDataBuffer, 0, 4);
}
}
///
/// Writes the pixel information to the stream.
///
/// The pixel format.
/// The pixel accessor.
/// The stream.
private void WriteDataChunks(PixelAccessor pixels, Stream stream)
where TColor : struct, IPackedPixel, IEquatable
{
int bytesPerScanline = this.width * this.bytesPerPixel;
byte[] previousScanline = new byte[bytesPerScanline];
byte[] rawScanline = new byte[bytesPerScanline];
int resultLength = bytesPerScanline + 1;
byte[] result = new byte[resultLength];
if (this.PngColorType != PngColorType.Palette)
{
this.sub = new byte[resultLength];
this.up = new byte[resultLength];
this.average = new byte[resultLength];
this.paeth = new byte[resultLength];
}
byte[] buffer;
int bufferLength;
MemoryStream memoryStream = null;
try
{
memoryStream = new MemoryStream();
using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel))
{
for (int y = 0; y < this.height; y++)
{
deflateStream.Write(this.EncodePixelRow(pixels, y, previousScanline, rawScanline, result), 0, resultLength);
Swap(ref rawScanline, ref previousScanline);
}
}
buffer = memoryStream.ToArray();
bufferLength = buffer.Length;
}
finally
{
memoryStream?.Dispose();
}
// Store the chunks in repeated 64k blocks.
// This reduces the memory load for decoding the image for many decoders.
int numChunks = bufferLength / MaxBlockSize;
if (bufferLength % MaxBlockSize != 0)
{
numChunks++;
}
for (int i = 0; i < numChunks; i++)
{
int length = bufferLength - (i * MaxBlockSize);
if (length > MaxBlockSize)
{
length = MaxBlockSize;
}
this.WriteChunk(stream, PngChunkTypes.Data, buffer, i * MaxBlockSize, length);
}
}
///
/// Writes the chunk end to the stream.
///
/// The containing image data.
private void WriteEndChunk(Stream stream)
{
this.WriteChunk(stream, PngChunkTypes.End, null);
}
///
/// Writes a chunk to the stream.
///
/// The to write to.
/// The type of chunk to write.
/// The containing data.
private void WriteChunk(Stream stream, string type, byte[] data)
{
this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
}
///
/// Writes a chunk of a specified length to the stream at the given offset.
///
/// The to write to.
/// The type of chunk to write.
/// The containing data.
/// The position to offset the data at.
/// The of the data to write.
private void WriteChunk(Stream stream, string type, byte[] data, int offset, int length)
{
WriteInteger(stream, length);
this.chunkTypeBuffer[0] = (byte)type[0];
this.chunkTypeBuffer[1] = (byte)type[1];
this.chunkTypeBuffer[2] = (byte)type[2];
this.chunkTypeBuffer[3] = (byte)type[3];
stream.Write(this.chunkTypeBuffer, 0, 4);
if (data != null)
{
stream.Write(data, offset, length);
}
this.crc.Reset();
this.crc.Update(this.chunkTypeBuffer);
if (data != null && length > 0)
{
this.crc.Update(data, offset, length);
}
WriteInteger(stream, (uint)this.crc.Value);
}
}
}