From 812ef6e4f201d93675afa1a97c02d51be2d51643 Mon Sep 17 00:00:00 2001 From: Ildar Date: Thu, 8 Aug 2019 16:18:11 +0300 Subject: [PATCH] #244 Add support for interlaced PNG encoding (#955) * #244 Implement interlaced PNG encoding * #244 Update documentations * #244 Remove comment * Cleanup * Update PngEncoderCore.cs --- src/ImageSharp/Formats/Png/Adam7.cs | 32 +- .../Formats/Png/IPngEncoderOptions.cs | 7 +- src/ImageSharp/Formats/Png/PngConstants.cs | 2 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 29 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 16 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 683 +++++++++--------- .../Formats/Png/PngEncoderHelpers.cs | 57 ++ .../Formats/Png/PngEncoderOptions.cs | 82 +++ .../Formats/Png/PngEncoderOptionsHelpers.cs | 152 ++++ .../Formats/Png/PngInterlaceMode.cs | 2 +- src/ImageSharp/Formats/Png/PngMetaData.cs | 18 +- .../Formats/Png/PngEncoderTests.cs | 52 +- .../Formats/Png/PngMetaDataTests.cs | 9 +- 13 files changed, 746 insertions(+), 395 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/PngEncoderHelpers.cs create mode 100644 src/ImageSharp/Formats/Png/PngEncoderOptions.cs create mode 100644 src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs 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;