diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs index cf79f5c679..166105b98b 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs @@ -9,6 +9,8 @@ namespace ImageProcessorCore.Formats using System.IO; using System.Threading.Tasks; + using ImageProcessorCore.Quantizers; + /// /// Image encoder for writing image data to a stream in png format. /// @@ -19,11 +21,17 @@ namespace ImageProcessorCore.Formats /// private const int MaxBlockSize = 65535; + /// + /// The number of bits required to encode the colors in the png. + /// + private byte bitDepth; + + private QuantizedImage quantized; + /// /// Gets or sets the quality of output for images. /// - /// Png is a lossless format so this is not used in this encoder. - public int Quality { get; set; } + public int Quality { get; set; } = int.MaxValue; /// public string MimeType => "image/png"; @@ -51,6 +59,16 @@ namespace ImageProcessorCore.Formats /// The gamma value of the image. public double Gamma { get; set; } = 2.2F; + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + /// public bool IsSupportedFileExtension(string extension) { @@ -83,18 +101,35 @@ namespace ImageProcessorCore.Formats 0, 8); + this.Quality = image.Quality.Clamp(1, int.MaxValue); + + this.bitDepth = this.Quality <= 256 + ? (byte)(this.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; + } + PngHeader header = new PngHeader { Width = image.Width, Height = image.Height, - ColorType = 6, // Each pixel is an R,G,B triple, followed by an alpha sample. - BitDepth = 8, + ColorType = (byte)(this.Quality <= 256 ? 3 : 6), // 3 = indexed, 6= Each pixel is an R,G,B triple, followed by an alpha sample. + BitDepth = this.bitDepth, FilterMethod = 0, // None CompressionMethod = 0, InterlaceMethod = 0 }; this.WriteHeaderChunk(stream, header); + this.WritePaletteChunk(stream, header, image); this.WritePhysicalChunk(stream, image); this.WriteGammaChunk(stream); this.WriteDataChunks(stream, image); @@ -144,6 +179,79 @@ namespace ImageProcessorCore.Formats stream.Write(buffer, 0, 4); } + /// + /// Writes the header chunk to the stream. + /// + /// The containing image data. + /// The . + private void WriteHeaderChunk(Stream stream, PngHeader header) + { + byte[] chunkData = new byte[13]; + + WriteInteger(chunkData, 0, header.Width); + WriteInteger(chunkData, 4, header.Height); + + chunkData[8] = header.BitDepth; + chunkData[9] = header.ColorType; + chunkData[10] = header.CompressionMethod; + chunkData[11] = header.FilterMethod; + chunkData[12] = header.InterlaceMethod; + + this.WriteChunk(stream, PngChunkTypes.Header, chunkData); + } + + /// + /// Writes the palette chunk to the stream. + /// + /// The containing image data. + /// The . + private void WritePaletteChunk(Stream stream, PngHeader header, ImageBase image) + { + if (this.Quality > 256) + { + return; + } + + if (this.Quantizer == null) + { + this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; + } + + // Quantize the image returning a palette. + this.quantized = this.Quantizer.Quantize(image, this.Quality); + + // Grab the palette and write it to the stream. + Bgra32[] palette = this.quantized.Palette; + int pixelCount = palette.Length; + + // Get max colors for bit depth. + int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3; + byte[] colorTable = new byte[colorTableLength]; + + Parallel.For(0, pixelCount, + i => + { + int offset = i * 3; + Bgra32 color = palette[i]; + + colorTable[offset] = color.R; + colorTable[offset + 1] = color.G; + colorTable[offset + 2] = color.B; + }); + + this.WriteChunk(stream, PngChunkTypes.Palette, colorTable); + + // Write the transparency data + if (this.quantized.TransparentIndex > -1) + { + byte[] buffer = BitConverter.GetBytes(this.quantized.TransparentIndex); + + Array.Reverse(buffer); + + this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, buffer); + } + } + /// /// Writes the physical dimension information to the stream. /// @@ -199,45 +307,86 @@ namespace ImageProcessorCore.Formats /// The image base. private void WriteDataChunks(Stream stream, ImageBase image) { + byte[] data; int imageWidth = image.Width; int imageHeight = image.Height; - byte[] data = new byte[(imageWidth * imageHeight * 4) + image.Height]; - - int rowLength = (imageWidth * 4) + 1; - - Parallel.For(0, imageHeight, y => + // Indexed image. + if (this.Quality <= 256) { - byte compression = 0; - if (y > 0) - { - compression = 2; - } + // TODO: I think I need to split then pad the beginning of each row. + // Split the array etc. Code below doesn't do this right. + // Time to read the spec... Again. + + //data = new byte[(imageWidth * imageHeight) + image.Height]; + //int rowLength = imageWidth; + + //Parallel.For(0, imageHeight, y => + //{ + // byte compression = 0; + // if (y > 0) + // { + // compression = 2; + // } + + // data[y * rowLength] = compression; + + // for (int x = 0; x < imageWidth; x++) + // { + // // Calculate the offset for the new array. + // int dataOffset = (y * rowLength) + x + 1; + // data[dataOffset + 1] = this.quantized.Pixels[(y * rowLength) + x]; + + // if (y > 0) + // { + // data[dataOffset] -= this.quantized.Pixels[((y - 1) * rowLength) + x]; + // } + // } + //}); + + // This outputs image but doesn't pad. + data = this.quantized.Pixels; + } + else + { + // TrueColor image. + data = new byte[(imageWidth * imageHeight * 4) + image.Height]; - data[y * rowLength] = compression; + int rowLength = (imageWidth * 4) + 1; - for (int x = 0; x < imageWidth; x++) + Parallel.For(0, imageHeight, y => { - Bgra32 color = Color.ToNonPremultiplied(image[x, y]); - - // Calculate the offset for the new array. - int dataOffset = (y * rowLength) + (x * 4) + 1; - data[dataOffset] = color.R; - data[dataOffset + 1] = color.G; - data[dataOffset + 2] = color.B; - data[dataOffset + 3] = color.A; - + byte compression = 0; if (y > 0) { - color = Color.ToNonPremultiplied(image[x, y - 1]); + compression = 2; + } + + data[y * rowLength] = compression; - data[dataOffset] -= color.R; - data[dataOffset + 1] -= color.G; - data[dataOffset + 2] -= color.B; - data[dataOffset + 3] -= color.A; + for (int x = 0; x < imageWidth; x++) + { + Bgra32 color = Color.ToNonPremultiplied(image[x, y]); + + // Calculate the offset for the new array. + int dataOffset = (y * rowLength) + (x * 4) + 1; + data[dataOffset] = color.R; + data[dataOffset + 1] = color.G; + data[dataOffset + 2] = color.B; + data[dataOffset + 3] = color.A; + + if (y > 0) + { + color = Color.ToNonPremultiplied(image[x, y - 1]); + + data[dataOffset] -= color.R; + data[dataOffset + 1] -= color.G; + data[dataOffset + 2] -= color.B; + data[dataOffset + 3] -= color.A; + } } - } - }); + }); + } byte[] buffer; int bufferLength; @@ -289,27 +438,6 @@ namespace ImageProcessorCore.Formats this.WriteChunk(stream, PngChunkTypes.End, null); } - /// - /// Writes the header chunk to the stream. - /// - /// The containing image data. - /// The . - private void WriteHeaderChunk(Stream stream, PngHeader header) - { - byte[] chunkData = new byte[13]; - - WriteInteger(chunkData, 0, header.Width); - WriteInteger(chunkData, 4, header.Height); - - chunkData[8] = header.BitDepth; - chunkData[9] = header.ColorType; - chunkData[10] = header.CompressionMethod; - chunkData[11] = header.FilterMethod; - chunkData[12] = header.InterlaceMethod; - - this.WriteChunk(stream, PngChunkTypes.Header, chunkData); - } - /// /// Writes a chunk to the stream. /// @@ -356,5 +484,18 @@ namespace ImageProcessorCore.Formats WriteInteger(stream, (uint)crc32.Value); } + + /// + /// Returns how many bits are required to store the specified number of colors. + /// Performs a Log2() on the value. + /// + /// The number of colors. + /// + /// The + /// + private int GetBitsNeededForColorDepth(int colors) + { + return (int)Math.Ceiling(Math.Log(colors, 2)); + } } } diff --git a/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs b/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs index 33df554423..9fbb7af35f 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs +++ b/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs @@ -68,6 +68,29 @@ } } + [Fact] + public void ImageCanSaveIndexedPng() + { + if (!Directory.Exists("TestOutput/Indexed")) + { + Directory.CreateDirectory("TestOutput/Indexed"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + + using (FileStream output = File.OpenWrite($"TestOutput/Indexed/{Path.GetFileNameWithoutExtension(file)}.png")) + { + image.Quality = 255; + image.Save(output, new PngFormat()); + } + } + } + } + [Fact] public void ImageCanConvertFormat() {