// -------------------------------------------------------------------------------------------------------------------- // // Copyright © James South and contributors. // Licensed under the Apache License, Version 2.0. // // // Image encoder for writing image data to a stream in png format. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Formats { using System; using System.IO; using ICSharpCode.SharpZipLib.Checksums; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; /// /// 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; } /// /// 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; } /// /// Gets the default file extension for this encoder. /// /// The default file extension for this encoder. public string Extension { get { return "PNG"; } } /// /// Indicates if the image encoder supports the specified /// file extension. /// /// The file extension. /// true, if the encoder supports the specified /// extensions; otherwise false. /// /// /// is null (Nothing in Visual Basic). /// is a string /// of length zero or contains only blanks. public bool IsSupportedFileExtension(string extension) { Guard.NotNullOrEmpty(extension, "extension"); extension = extension.StartsWith(".") ? extension.Substring(1) : extension; return extension.Equals("PNG", StringComparison.OrdinalIgnoreCase); } /// /// Encodes the data of the specified image and writes the result to /// the specified stream. /// /// The image, where the data should be get from. /// Cannot be null (Nothing in Visual Basic). /// The stream, where the image data should be written to. /// Cannot be null (Nothing in Visual Basic). /// /// is null (Nothing in Visual Basic). /// - or - /// is null (Nothing in Visual Basic). /// public void Encode(ImageBase image, Stream stream) { Guard.NotNull(image, "image"); Guard.NotNull(stream, "stream"); // Write the png header. stream.Write( new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, 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.DensityX > 0 && image.DensityY > 0) { int dpmX = (int)Math.Round(image.DensityX * 39.3700787d); int dpmY = (int)Math.Round(image.DensityY * 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) { byte[] 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) { byte[] 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 + 0] = pixels[pixelOffset + 2]; data[dataOffset + 1] = pixels[pixelOffset + 1]; data[dataOffset + 2] = pixels[pixelOffset + 0]; data[dataOffset + 3] = pixels[pixelOffset + 3]; if (y > 0) { int lastOffset = (((y - 1) * imageBase.Width) + x) * 4; data[dataOffset + 0] -= pixels[lastOffset + 2]; data[dataOffset + 1] -= pixels[lastOffset + 1]; data[dataOffset + 2] -= pixels[lastOffset + 0]; data[dataOffset + 3] -= pixels[lastOffset + 3]; } } } 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 { if (memoryStream != null) { 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 != null ? 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); } } }