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()
{