// // 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); } } }