From 1bab6db93e69734bf4242e77008bb7509aef0023 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 Nov 2019 19:59:30 +1100 Subject: [PATCH] !st pass cleanup of Deflater --- src/ImageSharp/Formats/Png/Zlib/Deflater.cs | 573 +++++------------- .../Formats/Png/Zlib/DeflaterThrowHelper.cs | 17 + .../Formats/Png/Zlib/ZlibDeflateStream.cs | 9 +- 3 files changed, 165 insertions(+), 434 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs index 90e25fb58e..26d1f9a45a 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -1,97 +1,92 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// using System; -using System.Collections.Generic; -using System.Text; +using System.Runtime.CompilerServices; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { /// - /// This is the Deflater class. The deflater class compresses input - /// with the deflate algorithm described in RFC 1951. It has several - /// compression levels and three different strategies described below. - /// - /// This class is not thread safe. This is inherent in the API, due - /// to the split of deflate and setInput. - /// - /// author of the original java version : Jochen Hoenicke + /// This class compresses input with the deflate algorithm described in RFC 1951. + /// It has several compression levels and three different strategies described below. /// public sealed class Deflater : IDisposable { - #region Deflater Documentation - - /* - * The Deflater can do the following state transitions: - * - * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. - * / | (2) (5) | - * / v (5) | - * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) - * \ | (3) | ,--------' - * | | | (3) / - * v v (5) v v - * (1) -> BUSY_STATE ----> FINISHING_STATE - * | (6) - * v - * FINISHED_STATE - * \_____________________________________/ - * | (7) - * v - * CLOSED_STATE - * - * (1) If we should produce a header we start in INIT_STATE, otherwise - * we start in BUSY_STATE. - * (2) A dictionary may be set only when we are in INIT_STATE, then - * we change the state as indicated. - * (3) Whether a dictionary is set or not, on the first call of deflate - * we change to BUSY_STATE. - * (4) -- intentionally left blank -- :) - * (5) FINISHING_STATE is entered, when flush() is called to indicate that - * there is no more INPUT. There are also states indicating, that - * the header wasn't written yet. - * (6) FINISHED_STATE is entered, when everything has been flushed to the - * internal pending output buffer. - * (7) At any time (7) - * - */ - - #endregion Deflater Documentation - - #region Public Constants - /// /// The best and slowest compression level. This tries to find very /// long and distant string repetitions. /// - public const int BEST_COMPRESSION = 9; + public const int BestCompression = 9; /// /// The worst but fastest compression level. /// - public const int BEST_SPEED = 1; + public const int BestSpeed = 1; /// /// The default compression level. /// - public const int DEFAULT_COMPRESSION = -1; + public const int DefaultCompression = -1; /// /// This level won't compress at all but output uncompressed blocks. /// - public const int NO_COMPRESSION = 0; + public const int NoCompression = 0; /// /// The compression method. This is the only method supported so far. /// There is no need to use this constant at all. /// - public const int DEFLATED = 8; + public const int Deflated = 8; - #endregion Public Constants + /// + /// Compression level. + /// + private int level; + + /// + /// The current state. + /// + private int state; + + private DeflaterPendingBuffer pending; + private DeflaterEngine engine; + private bool isDisposed; - #region Public Enum + private const int IsSetDict = 0x01; + private const int IsFlushing = 0x04; + private const int IsFinishing = 0x08; + private const int BusyState = 0x10; + private const int FlushingState = 0x14; + private const int FinishingState = 0x1c; + private const int FinishedState = 0x1e; + private const int ClosedState = 0x7f; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The compression level, a value between NoCompression and BestCompression. + /// + /// if level is out of range. + public Deflater(MemoryAllocator memoryAllocator, int level) + { + if (level == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + this.pending = new DeflaterPendingBuffer(memoryAllocator); + this.engine = new DeflaterEngine(this.pending, true); + this.engine.Strategy = DeflateStrategy.Default; + this.SetLevel(level); + this.Reset(); + } /// /// Compression Level as an enum for safer use @@ -102,202 +97,72 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The best and slowest compression level. This tries to find very /// long and distant string repetitions. /// - BEST_COMPRESSION = Deflater.BEST_COMPRESSION, + BestCompression = Deflater.BestCompression, /// /// The worst but fastest compression level. /// - BEST_SPEED = Deflater.BEST_SPEED, + BestSpeed = Deflater.BestSpeed, /// /// The default compression level. /// - DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION, + DefaultCompression = Deflater.DefaultCompression, /// /// This level won't compress at all but output uncompressed blocks. /// - NO_COMPRESSION = Deflater.NO_COMPRESSION, + NoCompression = Deflater.NoCompression, /// /// The compression method. This is the only method supported so far. /// There is no need to use this constant at all. /// - DEFLATED = Deflater.DEFLATED + Deflated = Deflater.Deflated } - #endregion Public Enum - - #region Local Constants - - private const int IS_SETDICT = 0x01; - private const int IS_FLUSHING = 0x04; - private const int IS_FINISHING = 0x08; - - private const int INIT_STATE = 0x00; - private const int SETDICT_STATE = 0x01; - - // private static int INIT_FINISHING_STATE = 0x08; - // private static int SETDICT_FINISHING_STATE = 0x09; - private const int BUSY_STATE = 0x10; - - private const int FLUSHING_STATE = 0x14; - private const int FINISHING_STATE = 0x1c; - private const int FINISHED_STATE = 0x1e; - private const int CLOSED_STATE = 0x7f; - - #endregion Local Constants - - #region Constructors - /// - /// Creates a new deflater with given compression level. + /// Gets a value indicating whetherthe stream was finished and no more output bytes + /// are available. /// - /// The memory allocator to use for buffer allocations. - /// - /// the compression level, a value between NO_COMPRESSION - /// and BEST_COMPRESSION. - /// - /// - /// true, if we should suppress the Zlib/RFC1950 header at the - /// beginning and the adler checksum at the end of the output. This is - /// useful for the GZIP/PKZIP formats. - /// - /// if lvl is out of range. - public Deflater(MemoryAllocator memoryAllocator, int level, bool noZlibHeaderOrFooter) - { - if (level == DEFAULT_COMPRESSION) - { - level = 6; - } - else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) - { - throw new ArgumentOutOfRangeException(nameof(level)); - } - - pending = new DeflaterPendingBuffer(memoryAllocator); - engine = new DeflaterEngine(pending, noZlibHeaderOrFooter); - this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; - SetStrategy(DeflateStrategy.Default); - SetLevel(level); - Reset(); - } + public bool IsFinished => (this.state == FinishedState) && this.pending.IsFlushed; - #endregion Constructors + /// + /// Gets a value indicating whether the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput => this.engine.NeedsInput(); /// /// Resets the deflater. The deflater acts afterwards as if it was /// just created with the same compression level and strategy as it /// had before. /// + [MethodImpl(InliningOptions.ShortMethod)] public void Reset() { - state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); - totalOut = 0; - pending.Reset(); - engine.Reset(); - } - - /// - /// Gets the current adler checksum of the data that was processed so far. - /// - public int Adler - { - get - { - return engine.Adler; - } - } - - /// - /// Gets the number of input bytes processed so far. - /// - public long TotalIn - { - get - { - return engine.TotalIn; - } - } - - /// - /// Gets the number of output bytes so far. - /// - public long TotalOut - { - get - { - return totalOut; - } + this.state = BusyState; + this.pending.Reset(); + this.engine.Reset(); } /// - /// Flushes the current input block. Further calls to deflate() will + /// Flushes the current input block. Further calls to Deflate() will /// produce enough output to inflate everything in the current input - /// block. This is not part of Sun's JDK so I have made it package - /// private. It is used by DeflaterOutputStream to implement - /// flush(). + /// block. It is used by DeflaterOutputStream to implement Flush(). /// - public void Flush() - { - state |= IS_FLUSHING; - } + [MethodImpl(InliningOptions.ShortMethod)] + public void Flush() => this.state |= IsFlushing; /// - /// Finishes the deflater with the current input block. It is an error - /// to give more input after this method was called. This method must + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must /// be called to force all bytes to be flushed. /// - public void Finish() - { - state |= (IS_FLUSHING | IS_FINISHING); - } - - /// - /// Returns true if the stream was finished and no more output bytes - /// are available. - /// - public bool IsFinished - { - get - { - return (state == FINISHED_STATE) && pending.IsFlushed; - } - } - - /// - /// Returns true, if the input buffer is empty. - /// You should then call setInput(). - /// NOTE: This method can also return true when the stream - /// was finished. - /// - public bool IsNeedingInput - { - get - { - return engine.NeedsInput(); - } - } - - /// - /// Sets the data which should be compressed next. This should be only - /// called when needsInput indicates that more input is needed. - /// If you call setInput when needsInput() returns false, the - /// previous input that is still pending will be thrown away. - /// The given byte array should not be changed, before needsInput() returns - /// true again. - /// This call is equivalent to setInput(input, 0, input.length). - /// - /// - /// the buffer containing the input data. - /// - /// - /// if the buffer was finished() or ended(). - /// - public void SetInput(byte[] input) - { - SetInput(input, 0, input.Length); - } + [MethodImpl(InliningOptions.ShortMethod)] + public void Finish() => this.state |= IsFlushing | IsFinishing; /// /// Sets the data which should be compressed next. This should be @@ -305,25 +170,21 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// The given byte array should not be changed, before needsInput() returns /// true again. /// - /// - /// the buffer containing the input data. - /// - /// - /// the start of the data. - /// - /// - /// the number of data bytes of input. - /// - /// - /// if the buffer was Finish()ed or if previous input is still pending. + /// The buffer containing the input data. + /// The start of the data. + /// The number of data bytes of input. + /// + /// if the buffer was finished or if previous input is still pending. /// + [MethodImpl(InliningOptions.ShortMethod)] public void SetInput(byte[] input, int offset, int count) { - if ((state & IS_FINISHING) != 0) + if ((this.state & IsFinishing) != 0) { - throw new InvalidOperationException("Finish() already called"); + DeflaterThrowHelper.ThrowAlreadyFinished(); } - engine.SetInput(input, offset, count); + + this.engine.SetInput(input, offset, count); } /// @@ -337,11 +198,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// public void SetLevel(int level) { - if (level == DEFAULT_COMPRESSION) + if (level == DefaultCompression) { level = 6; } - else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + else if (level < NoCompression || level > BestCompression) { throw new ArgumentOutOfRangeException(nameof(level)); } @@ -349,273 +210,127 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (this.level != level) { this.level = level; - engine.SetLevel(level); + this.engine.SetLevel(level); } } - /// - /// Get current compression level - /// - /// Returns the current compression level - public int GetLevel() - { - return level; - } - - /// - /// Sets the compression strategy. Strategy is one of - /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact - /// position where the strategy is changed, the same as for - /// SetLevel() applies. - /// - /// - /// The new compression strategy. - /// - public void SetStrategy(DeflateStrategy strategy) - { - engine.Strategy = strategy; - } - - /// - /// Deflates the current input block with to the given array. - /// - /// - /// The buffer where compressed data is stored - /// - /// - /// The number of compressed bytes added to the output, or 0 if either - /// IsNeedingInput() or IsFinished returns true or length is zero. - /// - public int Deflate(byte[] output) - { - return Deflate(output, 0, output.Length); - } - /// /// Deflates the current input block to the given array. /// - /// - /// Buffer to store the compressed data. - /// - /// - /// Offset into the output array. - /// - /// - /// The maximum number of bytes that may be stored. - /// + /// Buffer to store the compressed data. + /// Offset into the output array. + /// The maximum number of bytes that may be stored. /// /// The number of compressed bytes added to the output, or 0 if either - /// needsInput() or finished() returns true or length is zero. + /// or returns true or length is zero. /// - /// - /// If Finish() was previously called. - /// - /// - /// If offset or length don't match the array length. - /// public int Deflate(byte[] output, int offset, int length) { int origLength = length; - if (state == CLOSED_STATE) + if (this.state == ClosedState) { - throw new InvalidOperationException("Deflater closed"); + DeflaterThrowHelper.ThrowAlreadyClosed(); } - if (state < BUSY_STATE) + if (this.state < BusyState) { - // output header - int header = (DEFLATED + - ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; - int level_flags = (level - 1) >> 1; - if (level_flags < 0 || level_flags > 3) + // Output header + int header = (Deflated + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int levelFlags = (this.level - 1) >> 1; + if (levelFlags < 0 || levelFlags > 3) { - level_flags = 3; + levelFlags = 3; } - header |= level_flags << 6; - if ((state & IS_SETDICT) != 0) + + header |= levelFlags << 6; + if ((this.state & IsSetDict) != 0) { // Dictionary was set header |= DeflaterConstants.PRESET_DICT; } + header += 31 - (header % 31); - pending.WriteShortMSB(header); - if ((state & IS_SETDICT) != 0) + this.pending.WriteShortMSB(header); + if ((this.state & IsSetDict) != 0) { - int chksum = engine.Adler; - engine.ResetAdler(); - pending.WriteShortMSB(chksum >> 16); - pending.WriteShortMSB(chksum & 0xffff); + int chksum = this.engine.Adler; + this.engine.ResetAdler(); + this.pending.WriteShortMSB(chksum >> 16); + this.pending.WriteShortMSB(chksum & 0xffff); } - state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + this.state = BusyState | (this.state & (IsFlushing | IsFinishing)); } - for (; ; ) + while (true) { - int count = pending.Flush(output, offset, length); + int count = this.pending.Flush(output, offset, length); offset += count; - totalOut += count; length -= count; - if (length == 0 || state == FINISHED_STATE) + if (length == 0 || this.state == FinishedState) { break; } - if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) + if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) { - switch (state) + switch (this.state) { - case BUSY_STATE: + case BusyState: // We need more input now return origLength - length; - case FLUSHING_STATE: - if (level != NO_COMPRESSION) + case FlushingState: + if (this.level != NoCompression) { - /* We have to supply some lookahead. 8 bit lookahead - * is needed by the zlib inflater, and we must fill - * the next byte, so that all bits are flushed. - */ - int neededbits = 8 + ((-pending.BitCount) & 7); + // We have to supply some lookahead. 8 bit lookahead + // is needed by the zlib inflater, and we must fill + // the next byte, so that all bits are flushed. + int neededbits = 8 + ((-this.pending.BitCount) & 7); while (neededbits > 0) { - /* write a static tree block consisting solely of - * an EOF: - */ - pending.WriteBits(2, 10); + // Write a static tree block consisting solely of an EOF: + this.pending.WriteBits(2, 10); neededbits -= 10; } } - state = BUSY_STATE; - break; - case FINISHING_STATE: - pending.AlignToByte(); + this.state = BusyState; + break; - // Compressed data is complete. Write footer information if required. - if (!noZlibHeaderOrFooter) - { - int adler = engine.Adler; - pending.WriteShortMSB(adler >> 16); - pending.WriteShortMSB(adler & 0xffff); - } - state = FINISHED_STATE; + case FinishingState: + this.pending.AlignToByte(); + this.state = FinishedState; break; } } } - return origLength - length; - } - /// - /// Sets the dictionary which should be used in the deflate process. - /// This call is equivalent to setDictionary(dict, 0, dict.Length). - /// - /// - /// the dictionary. - /// - /// - /// if SetInput () or Deflate () were already called or another dictionary was already set. - /// - public void SetDictionary(byte[] dictionary) - { - SetDictionary(dictionary, 0, dictionary.Length); + return origLength - length; } - /// - /// Sets the dictionary which should be used in the deflate process. - /// The dictionary is a byte array containing strings that are - /// likely to occur in the data which should be compressed. The - /// dictionary is not stored in the compressed output, only a - /// checksum. To decompress the output you need to supply the same - /// dictionary again. - /// - /// - /// The dictionary data - /// - /// - /// The index where dictionary information commences. - /// - /// - /// The number of bytes in the dictionary. - /// - /// - /// If SetInput () or Deflate() were already called or another dictionary was already set. - /// - public void SetDictionary(byte[] dictionary, int index, int count) + /// + public void Dispose() { - if (state != INIT_STATE) - { - throw new InvalidOperationException(); - } - - state = SETDICT_STATE; - engine.SetDictionary(dictionary, index, count); + this.Dispose(true); + GC.SuppressFinalize(this); } - #region Instance Fields - - /// - /// Compression level. - /// - private int level; - - /// - /// If true no Zlib/RFC1950 headers or footers are generated - /// - private bool noZlibHeaderOrFooter; - - /// - /// The current state. - /// - private int state; - - /// - /// The total bytes of output written. - /// - private long totalOut; - - /// - /// The pending output. - /// - private DeflaterPendingBuffer pending; - - /// - /// The deflater engine. - /// - private DeflaterEngine engine; - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - void Dispose(bool disposing) + private void Dispose(bool disposing) { - if (!disposedValue) + if (!this.isDisposed) { if (disposing) { this.pending.Dispose(); - // TODO: dispose managed state (managed objects). } - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. this.pending = null; - disposedValue = true; + this.isDisposed = true; } } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - - #endregion Instance Fields } } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs new file mode 100644 index 0000000000..b59d32c4d1 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterThrowHelper.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + internal static class DeflaterThrowHelper + { + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 69b3c06026..5724e027d2 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.IO.Compression; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib @@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // +---+---+ // |CMF|FLG| // +---+---+ - int cmf = 0x78; + const int Cmf = 0x78; int flg = 218; // http://stackoverflow.com/a/2331025/277304 @@ -83,14 +82,14 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib } // Just in case - flg -= ((cmf * 256) + flg) % 31; + flg -= ((Cmf * 256) + flg) % 31; if (flg < 0) { flg += 31; } - this.rawStream.WriteByte((byte)cmf); + this.rawStream.WriteByte(Cmf); this.rawStream.WriteByte((byte)flg); // Initialize the deflate Stream. @@ -104,7 +103,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib // { // level = CompressionLevel.NoCompression; // } - this.deflater = new Deflater(memoryAllocator, compressionLevel, true); + this.deflater = new Deflater(memoryAllocator, compressionLevel); this.deflateStream = new DeflaterOutputStream(this.rawStream, this.deflater) { IsStreamOwner = false }; // this.deflateStream = new DeflateStream(this.rawStream, level, true);