// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageProcessor.Formats { using System; using System.IO; /// /// Image encoder for writing image data to a stream in png format. /// public class PngEncoder : IImageEncoder { /// /// The maximum block size. /// private const int MaxBlockSize = 0xFFFF; /// /// Initializes a new instance of the class. /// public PngEncoder() { this.Gamma = 2.2f; } /// /// 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 string MimeType => "image/png"; /// public string Extension => "png"; /// /// Gets or sets a value indicating whether this encoder /// will write the image uncompressed the stream. /// /// /// true if the image should be written uncompressed to /// the stream; otherwise, false. /// public bool IsWritingUncompressed { get; set; } /// /// Gets or sets a value indicating whether this instance is writing /// gamma information to the stream. The default value is false. /// /// /// True if this instance is writing gamma /// information to the stream.; otherwise, false. /// public bool IsWritingGamma { 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; } /// public bool IsSupportedFileExtension(string extension) { Guard.NotNullOrEmpty(extension, nameof(extension)); extension = extension.StartsWith(".") ? extension.Substring(1) : extension; return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase); } /// 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); PngHeader header = new PngHeader { Width = image.Width, Height = image.Height, ColorType = 6, BitDepth = 8, FilterMethod = 0, CompressionMethod = 0, InterlaceMethod = 0 }; this.WriteHeaderChunk(stream, header); this.WritePhysicalChunk(stream, image); this.WriteGammaChunk(stream); if (this.IsWritingUncompressed) { this.WriteDataChunksFast(stream, image); } else { 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 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) { 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.IsWritingGamma) { 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 WriteDataChunksFast(Stream stream, ImageBase imageBase) { float[] pixels = imageBase.Pixels; // Convert the pixel array to a new array for adding // the filter byte. byte[] data = new byte[(imageBase.Width * imageBase.Height * 4) + imageBase.Height]; int rowLength = (imageBase.Width * 4) + 1; for (int y = 0; y < imageBase.Height; y++) { data[y * rowLength] = 0; Array.Copy(pixels, y * imageBase.Width * 4, data, (y * rowLength) + 1, imageBase.Width * 4); } Adler32 adler32 = new Adler32(); adler32.Update(data); using (MemoryStream tempStream = new MemoryStream()) { int remainder = data.Length; int blockCount; if ((data.Length % MaxBlockSize) == 0) { blockCount = data.Length / MaxBlockSize; } else { blockCount = (data.Length / MaxBlockSize) + 1; } // Write headers tempStream.WriteByte(0x78); tempStream.WriteByte(0xDA); for (int i = 0; i < blockCount; i++) { // Write the length ushort length = (ushort)((remainder < MaxBlockSize) ? remainder : MaxBlockSize); tempStream.WriteByte(length == remainder ? (byte)0x01 : (byte)0x00); tempStream.Write(BitConverter.GetBytes(length), 0, 2); // Write one's compliment of length tempStream.Write(BitConverter.GetBytes((ushort)~length), 0, 2); // Write blocks tempStream.Write(data, i * MaxBlockSize, length); // Next block remainder -= MaxBlockSize; } WriteInteger(tempStream, (int)adler32.Value); tempStream.Seek(0, SeekOrigin.Begin); byte[] zipData = new byte[tempStream.Length]; tempStream.Read(zipData, 0, (int)tempStream.Length); this.WriteChunk(stream, PngChunkTypes.Data, zipData); } } /// /// Writes the pixel information to the stream. /// /// The containing image data. /// The image base. private void WriteDataChunks(Stream stream, ImageBase imageBase) { float[] pixels = imageBase.Pixels; byte[] data = new byte[(imageBase.Width * imageBase.Height * 4) + imageBase.Height]; int rowLength = (imageBase.Width * 4) + 1; for (int y = 0; y < imageBase.Height; y++) { byte compression = 0; if (y > 0) { compression = 2; } data[y * rowLength] = compression; for (int x = 0; x < imageBase.Width; x++) { // Calculate the offset for the new array. int dataOffset = (y * rowLength) + (x * 4) + 1; // Calculate the offset for the original pixel array. int pixelOffset = ((y * imageBase.Width) + x) * 4; data[dataOffset] = (byte)(pixels[pixelOffset].Clamp(0, 1) * 255); data[dataOffset + 1] = (byte)(pixels[pixelOffset + 1].Clamp(0, 1) * 255); data[dataOffset + 2] = (byte)(pixels[pixelOffset + 2].Clamp(0, 1) * 255); data[dataOffset + 3] = (byte)(pixels[pixelOffset + 3].Clamp(0, 1) * 255); if (y > 0) { int lastOffset = (((y - 1) * imageBase.Width) + x) * 4; data[dataOffset] -= (byte)(pixels[lastOffset].Clamp(0, 1) * 255); data[dataOffset + 1] -= (byte)(pixels[lastOffset + 1].Clamp(0, 1) * 255); data[dataOffset + 2] -= (byte)(pixels[lastOffset + 2].Clamp(0, 1) * 255); data[dataOffset + 3] -= (byte)(pixels[lastOffset + 3].Clamp(0, 1) * 255); } } } byte[] buffer; int bufferLength; MemoryStream memoryStream = null; try { memoryStream = new MemoryStream(); using (DeflaterOutputStream outputStream = new DeflaterOutputStream(memoryStream)) { outputStream.Write(data, 0, data.Length); outputStream.Flush(); outputStream.Finish(); 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 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. /// /// 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); } } }