From 1f0affd73294b75f1c7672c8905b21262a241a48 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 21 Jan 2016 18:09:28 +1100 Subject: [PATCH] Png now decodes using MS Deflate stream Former-commit-id: d753c8b0bdb066f56e4e48423e7a58dc3702c03d Former-commit-id: 019e60e30b144148b895910791ddd807f6e637d3 Former-commit-id: 68825fd0d9534498cf2ff589854e01e518e9d246 --- .../Formats/Png/PngDecoderCore.cs | 5 +- src/ImageProcessor/Formats/Png/PngEncoder.cs | 13 +- src/ImageProcessor/Formats/Png/Zlib/Crc32.cs | 2 +- .../Formats/Png/Zlib/IChecksum.cs | 4 +- .../Png/Zlib/NewFolder/ZlibInputStream.cs | 210 ++++++++++++++++++ .../Png/Zlib/NewFolder/ZlibOutputStream.cs | 205 +++++++++++++++++ 6 files changed, 432 insertions(+), 7 deletions(-) create mode 100644 src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibInputStream.cs create mode 100644 src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibOutputStream.cs diff --git a/src/ImageProcessor/Formats/Png/PngDecoderCore.cs b/src/ImageProcessor/Formats/Png/PngDecoderCore.cs index 85ed12c12f..489d69eebe 100644 --- a/src/ImageProcessor/Formats/Png/PngDecoderCore.cs +++ b/src/ImageProcessor/Formats/Png/PngDecoderCore.cs @@ -138,7 +138,7 @@ namespace ImageProcessor.Formats if (this.header.Width > ImageBase.MaxWidth || this.header.Height > ImageBase.MaxHeight) { throw new ArgumentOutOfRangeException( - $"The input png '{this.header.Width}x{this.header.Height}' is bigger thean the " + $"The input png '{this.header.Width}x{this.header.Height}' is bigger than the " + $"max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'"); } @@ -260,7 +260,8 @@ namespace ImageProcessor.Formats byte[] currentScanline = new byte[scanlineLength]; int filter = 0, column = -1; - using (InflaterInputStream compressedStream = new InflaterInputStream(dataStream)) + using (ZlibInputStream compressedStream = new ZlibInputStream(dataStream)) + //using (InflaterInputStream compressedStream = new InflaterInputStream(dataStream)) { int readByte; while ((readByte = compressedStream.ReadByte()) >= 0) diff --git a/src/ImageProcessor/Formats/Png/PngEncoder.cs b/src/ImageProcessor/Formats/Png/PngEncoder.cs index e4501a9560..ba404518d7 100644 --- a/src/ImageProcessor/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessor/Formats/Png/PngEncoder.cs @@ -38,6 +38,13 @@ namespace ImageProcessor.Formats /// public string Extension => "png"; + /// + /// The compression level 1-9. + /// TODO: Get other compression levels to work. Something is cutting of image content. + /// Defaults to 6. + /// + public int CompressionLevel { get; set; } = 6; + /// /// Gets or sets a value indicating whether this encoder /// will write the image uncompressed the stream. @@ -56,7 +63,7 @@ namespace ImageProcessor.Formats /// /// /// True if this instance is writing gamma - /// information to the stream.; otherwise, false. + /// information to the stream; otherwise, false. /// public bool IsWritingGamma { get; set; } @@ -95,7 +102,7 @@ namespace ImageProcessor.Formats 0x0D, // Line ending CRLF 0x0A, // Line ending CRLF 0x1A, // EOF - 0x0A // LF + 0x0A // LF }, 0, 8); @@ -360,6 +367,8 @@ namespace ImageProcessor.Formats { memoryStream = new MemoryStream(); + // TODO: Get this working! + //using (ZlibOutputStream outputStream = new ZlibOutputStream(memoryStream, this.CompressionLevel)) using (DeflaterOutputStream outputStream = new DeflaterOutputStream(memoryStream)) { outputStream.Write(data, 0, data.Length); diff --git a/src/ImageProcessor/Formats/Png/Zlib/Crc32.cs b/src/ImageProcessor/Formats/Png/Zlib/Crc32.cs index 3828260b40..70cceb0a2f 100644 --- a/src/ImageProcessor/Formats/Png/Zlib/Crc32.cs +++ b/src/ImageProcessor/Formats/Png/Zlib/Crc32.cs @@ -104,7 +104,7 @@ namespace ImageProcessor.Formats private uint crc; /// - /// Returns the CRC32 data checksum computed so far. + /// Gets or sets the CRC32 data checksum computed so far. /// public long Value { diff --git a/src/ImageProcessor/Formats/Png/Zlib/IChecksum.cs b/src/ImageProcessor/Formats/Png/Zlib/IChecksum.cs index 18dbe5edff..7e6d65be1c 100644 --- a/src/ImageProcessor/Formats/Png/Zlib/IChecksum.cs +++ b/src/ImageProcessor/Formats/Png/Zlib/IChecksum.cs @@ -15,7 +15,7 @@ namespace ImageProcessor.Formats public interface IChecksum { /// - /// Returns the data checksum computed so far. + /// Gets the data checksum computed so far. /// long Value { @@ -31,7 +31,7 @@ namespace ImageProcessor.Formats /// Adds one byte to the data checksum. /// /// - /// the data value to add. The high byte of the int is ignored. + /// The data value to add. The high byte of the integer is ignored. /// void Update(int value); diff --git a/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibInputStream.cs b/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibInputStream.cs new file mode 100644 index 0000000000..129042bda1 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibInputStream.cs @@ -0,0 +1,210 @@ + +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + using System.IO.Compression; + + internal class ZlibInputStream : Stream + { + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// The preset dictionary. + /// Merely informational, not used. + /// + private bool fdict; + + /// + /// The DICT dictionary identifier identifying the used dictionary. + /// Merely informational, not used. + /// + private byte[] dictId; + + /// + /// CINFO is the base-2 logarithm of the LZ77 window size, minus eight. + /// Merely informational, not used. + /// + private int cinfo; + + /// + /// The read crc data. + /// + private byte[] crcread; + + // The stream responsible for decompressing the input stream. + private DeflateStream deflateStream; + + public ZlibInputStream(Stream stream) + { + this.rawStream = stream; + + // Read the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = this.rawStream.ReadByte(); + int flag = this.rawStream.ReadByte(); + if (cmf == -1 || flag == -1) + { + return; + } + + if ((cmf & 0x0f) != 8) + { + throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}"); + } + + this.cinfo = ((cmf & (0xf0)) >> 8); + this.fdict = (flag & 32) != 0; + + if (this.fdict) + { + this.dictId = new byte[4]; + + for (int i = 0; i < 4; i++) + { + // We consume but don't use this. + this.dictId[i] = (byte)this.rawStream.ReadByte(); + } + } + + // Initialize the deflate Stream. + this.deflateStream = new DeflateStream(this.rawStream, CompressionMode.Decompress, true); + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + /// + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + /// + public override void Flush() + { + this.deflateStream?.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + // We dont't check CRC on reading + int read = this.deflateStream.Read(buffer, offset, count); + if (read < 1 && this.crcread == null) + { + // The deflater has ended. We try to read the next 4 bytes from raw stream (crc) + this.crcread = new byte[4]; + for (int i = 0; i < 4; i++) + { + // we dont really check/use this + this.crcread[i] = (byte)rawStream.ReadByte(); + } + } + + return read; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + if (this.deflateStream != null) + { + this.deflateStream.Dispose(); + this.deflateStream = null; + + if (this.crcread == null) + { + // Consume the trailing 4 bytes + this.crcread = new byte[4]; + for (int i = 0; i < 4; i++) + { + this.crcread[i] = (byte)this.rawStream.ReadByte(); + } + } + } + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + } +} diff --git a/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibOutputStream.cs b/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibOutputStream.cs new file mode 100644 index 0000000000..258c13f0e0 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/Zlib/NewFolder/ZlibOutputStream.cs @@ -0,0 +1,205 @@ + +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + using System.IO.Compression; + + internal class ZlibOutputStream : Stream + { + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// Computes the checksum for the data stream. + /// + private readonly Adler32 adler32 = new Adler32(); + + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + // The stream responsible for decompressing the input stream. + private DeflateStream deflateStream; + + /// + /// Initializes a new instance of + /// + /// The stream to compress. + /// The compression level. + public ZlibOutputStream(Stream stream, int compressionLevel) + { + this.rawStream = stream; + + // Write the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // 0 1 + // + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = 0x78; + int flg = 218; + + // http://stackoverflow.com/a/2331025/277304 + if (compressionLevel >= 5 && compressionLevel <= 6) + { + flg = 156; + } + else if (compressionLevel >= 3 && compressionLevel <= 4) + { + flg = 94; + } + + else if (compressionLevel <= 2) + { + flg = 1; + } + + // Just in case + flg -= (cmf * 256 + flg) % 31; + + if (flg < 0) + { + flg += 31; + } + + this.rawStream.WriteByte((byte)cmf); + this.rawStream.WriteByte((byte)flg); + + // Initialize the deflate Stream. + CompressionLevel level = CompressionLevel.Optimal; + + if (compressionLevel >= 1 && compressionLevel <= 5) + { + level = CompressionLevel.Fastest; + } + + else if (compressionLevel == 0) + { + level = CompressionLevel.NoCompression; + } + + // I must create with leaveopen=true always and do the closing myself, because MS implementation + // of DeflateStream: I cant force a flush of the underlying stream without closing. + this.deflateStream = new DeflateStream(this.rawStream, level, true); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + /// + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + /// + public override void Flush() + { + this.deflateStream?.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + this.deflateStream.Write(buffer, offset, count); + this.adler32.Update(buffer, offset, count); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + if (this.deflateStream != null) + { + this.deflateStream.Dispose(); + this.deflateStream = null; + } + else { + + // Second hack: empty input? + this.rawStream.WriteByte(3); + this.rawStream.WriteByte(0); + } + + // Add the crc + uint crc = (uint)this.adler32.Value; + this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF)); + this.rawStream.WriteByte((byte)((crc) & 0xFF)); + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + } +}