diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs index 166105b98..170144ca7 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs @@ -7,7 +7,6 @@ namespace ImageProcessorCore.Formats { using System; using System.IO; - using System.Threading.Tasks; using ImageProcessorCore.Quantizers; @@ -16,18 +15,6 @@ namespace ImageProcessorCore.Formats /// public class PngEncoder : IImageEncoder { - /// - /// The maximum block size, defaults at 64k for uncompressed blocks. - /// - 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. /// @@ -45,19 +32,13 @@ namespace ImageProcessorCore.Formats /// 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 double Gamma { get; set; } = 2.2F; + public float Gamma { get; set; } = 2.2F; /// /// The quantizer for reducing the color count. @@ -69,6 +50,12 @@ namespace ImageProcessorCore.Formats /// public byte Threshold { get; set; } = 128; + /// + /// 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; } + /// public bool IsSupportedFileExtension(string extension) { @@ -82,420 +69,17 @@ namespace ImageProcessorCore.Formats /// public void Encode(ImageBase image, Stream stream) { - Guard.NotNull(image, nameof(image)); - Guard.NotNull(stream, nameof(stream)); - - // Write the png header. - stream.Write( - new byte[] - { - 0x89, // Set the high bit. - 0x50, // P - 0x4E, // N - 0x47, // G - 0x0D, // Line ending CRLF - 0x0A, // Line ending CRLF - 0x1A, // EOF - 0x0A // LF - }, - 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 = (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 + PngEncoderCore encoder = new PngEncoderCore + { + CompressionLevel = this.CompressionLevel, + Gamma = this.Gamma, + Quality = this.Quality, + Quantizer = this.Quantizer, + WriteGamma = this.WriteGamma, + Threshold = this.Threshold }; - this.WriteHeaderChunk(stream, header); - this.WritePaletteChunk(stream, header, image); - this.WritePhysicalChunk(stream, image); - this.WriteGammaChunk(stream); - this.WriteDataChunks(stream, image); - 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); - - Array.Reverse(buffer); - Array.Copy(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); - - Array.Reverse(buffer); - - 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); - - Array.Reverse(buffer); - - 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. - /// - /// The containing image data. - /// The image base. - private void WritePhysicalChunk(Stream stream, ImageBase imageBase) - { - Image image = imageBase as Image; - if (image != null && image.HorizontalResolution > 0 && image.VerticalResolution > 0) - { - // 39.3700787 = inches in a meter. - int dpmX = (int)Math.Round(image.HorizontalResolution * 39.3700787d); - int dpmY = (int)Math.Round(image.VerticalResolution * 39.3700787d); - - byte[] chunkData = new byte[9]; - - WriteInteger(chunkData, 0, dpmX); - WriteInteger(chunkData, 4, dpmY); - - chunkData[8] = 1; - - this.WriteChunk(stream, PngChunkTypes.Physical, chunkData); - } - } - - /// - /// 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[] fourByteData = new byte[4]; - - byte[] size = BitConverter.GetBytes(gammaValue); - - fourByteData[0] = size[3]; - fourByteData[1] = size[2]; - fourByteData[2] = size[1]; - fourByteData[3] = size[0]; - - this.WriteChunk(stream, PngChunkTypes.Gamma, fourByteData); - } - } - - /// - /// Writes the pixel information to the stream. - /// - /// The containing image data. - /// The image base. - private void WriteDataChunks(Stream stream, ImageBase image) - { - byte[] data; - int imageWidth = image.Width; - int imageHeight = image.Height; - - // Indexed image. - if (this.Quality <= 256) - { - // 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]; - - int rowLength = (imageWidth * 4) + 1; - - Parallel.For(0, imageHeight, y => - { - byte compression = 0; - if (y > 0) - { - compression = 2; - } - - data[y * rowLength] = compression; - - 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; - - MemoryStream memoryStream = null; - try - { - memoryStream = new MemoryStream(); - - using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel)) - { - deflateStream.Write(data, 0, data.Length); - } - - bufferLength = (int)memoryStream.Length; - buffer = memoryStream.ToArray(); - } - finally - { - memoryStream?.Dispose(); - } - - 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); - - byte[] typeArray = new byte[4]; - typeArray[0] = (byte)type[0]; - typeArray[1] = (byte)type[1]; - typeArray[2] = (byte)type[2]; - typeArray[3] = (byte)type[3]; - - stream.Write(typeArray, 0, 4); - - if (data != null) - { - stream.Write(data, offset, length); - } - - Crc32 crc32 = new Crc32(); - crc32.Update(typeArray); - - if (data != null) - { - crc32.Update(data, offset, length); - } - - 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)); + encoder.Encode(image, stream); } } } diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs new file mode 100644 index 000000000..ada5ec153 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -0,0 +1,480 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + using System.Threading.Tasks; + + using ImageProcessorCore.Quantizers; + + /// + /// Performs the png encoding operation. + /// TODO: Perf. There's lots of array parsing going on here. This should be unmanaged. + /// + internal class PngEncoderCore + { + /// + /// The maximum block size, defaults at 64k for uncompressed blocks. + /// + private const int MaxBlockSize = 65535; + + /// + /// The number of bits required to encode the colors in the png. + /// + private byte bitDepth; + + /// + /// The quantized image result. + /// + private QuantizedImage quantized; + + /// + /// Gets or sets the quality of output for images. + /// + public int Quality { get; set; } = int.MaxValue; + + /// + /// 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; + + /// + /// 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 void Encode(ImageBase image, Stream stream) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + // Write the png header. + stream.Write( + new byte[] + { + 0x89, // Set the high bit. + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // Line ending CRLF + 0x0A, // Line ending CRLF + 0x1A, // EOF + 0x0A // LF + }, + 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; + } + + // TODO: Add more color options here. + PngHeader header = new PngHeader + { + Width = image.Width, + Height = image.Height, + 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); + 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); + + Array.Reverse(buffer); + Array.Copy(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); + + Array.Reverse(buffer); + + 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); + + Array.Reverse(buffer); + + 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 . + /// The image to encode. + 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. + /// + /// The containing image data. + /// The image base. + private void WritePhysicalChunk(Stream stream, ImageBase imageBase) + { + Image image = imageBase as Image; + if (image != null && image.HorizontalResolution > 0 && image.VerticalResolution > 0) + { + // 39.3700787 = inches in a meter. + int dpmX = (int)Math.Round(image.HorizontalResolution * 39.3700787d); + int dpmY = (int)Math.Round(image.VerticalResolution * 39.3700787d); + + byte[] chunkData = new byte[9]; + + WriteInteger(chunkData, 0, dpmX); + WriteInteger(chunkData, 4, dpmY); + + chunkData[8] = 1; + + this.WriteChunk(stream, PngChunkTypes.Physical, chunkData); + } + } + + /// + /// 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[] fourByteData = new byte[4]; + + byte[] size = BitConverter.GetBytes(gammaValue); + + fourByteData[0] = size[3]; + fourByteData[1] = size[2]; + fourByteData[2] = size[1]; + fourByteData[3] = size[0]; + + this.WriteChunk(stream, PngChunkTypes.Gamma, fourByteData); + } + } + + /// + /// Writes the pixel information to the stream. + /// + /// The containing image data. + /// The image base. + private void WriteDataChunks(Stream stream, ImageBase image) + { + byte[] data; + int imageWidth = image.Width; + int imageHeight = image.Height; + + // Indexed image. + if (this.Quality <= 256) + { + int rowLength = imageWidth + 1; + data = new byte[rowLength * imageHeight]; + + Parallel.For(0, imageHeight, y => + { + int dataOffset = (y * rowLength); + byte compression = 0; + if (y > 0) + { + compression = 2; + } + data[dataOffset++] = compression; + for (int x = 0; x < imageWidth; x++) + { + data[dataOffset++] = this.quantized.Pixels[(y * imageWidth) + x]; + if (y > 0) + { + data[dataOffset - 1] -= this.quantized.Pixels[((y - 1) * imageWidth) + x]; + } + } + }); + } + else + { + // TrueColor image. + data = new byte[(imageWidth * imageHeight * 4) + image.Height]; + + int rowLength = (imageWidth * 4) + 1; + + Parallel.For(0, imageHeight, y => + { + byte compression = 0; + if (y > 0) + { + compression = 2; + } + + data[y * rowLength] = compression; + + 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; + + MemoryStream memoryStream = null; + try + { + memoryStream = new MemoryStream(); + + using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel)) + { + deflateStream.Write(data, 0, data.Length); + } + + bufferLength = (int)memoryStream.Length; + buffer = memoryStream.ToArray(); + } + finally + { + memoryStream?.Dispose(); + } + + 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); + + byte[] typeArray = new byte[4]; + typeArray[0] = (byte)type[0]; + typeArray[1] = (byte)type[1]; + typeArray[2] = (byte)type[2]; + typeArray[3] = (byte)type[3]; + + stream.Write(typeArray, 0, 4); + + if (data != null) + { + stream.Write(data, offset, length); + } + + Crc32 crc32 = new Crc32(); + crc32.Update(typeArray); + + if (data != null) + { + crc32.Update(data, offset, length); + } + + 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)); + } + } +}