diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 09575bb28..19c6af27f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -699,7 +699,7 @@ namespace SixLabors.ImageSharp.Formats.Png { using (var memoryStream = new MemoryStream()) { - using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel)) + using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { deflateStream.Write(textBytes); } @@ -790,7 +790,7 @@ namespace SixLabors.ImageSharp.Formats.Png using (var memoryStream = new MemoryStream()) { - using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel)) + using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { if (this.options.InterlaceMethod == PngInterlaceMode.Adam7) { diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs index a06983b9e..c4dc82a4d 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs @@ -1,8 +1,9 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -112,7 +113,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(ReadOnlySpan data) { - // (By Per Bothner) + ref byte dataRef = ref MemoryMarshal.GetReference(data); uint s1 = this.checksum & 0xFFFF; uint s2 = this.checksum >> 16; @@ -133,8 +134,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib count -= n; while (--n >= 0) { - s1 = s1 + (uint)(data[offset++] & 0xff); - s2 = s2 + s1; + s1 += Unsafe.Add(ref dataRef, offset++); + s2 += s1; } s1 %= Base; @@ -144,4 +145,4 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.checksum = (s2 << 16) | s1; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs index d1588c384..77355e908 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs @@ -1,8 +1,9 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -141,9 +142,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.crc ^= CrcSeed; + ref uint crcTableRef = ref MemoryMarshal.GetReference(CrcTable.AsSpan()); for (int i = 0; i < data.Length; i++) { - this.crc = CrcTable[(this.crc ^ data[i]) & 0xFF] ^ (this.crc >> 8); + this.crc = Unsafe.Add(ref crcTableRef, (int)((this.crc ^ data[i]) & 0xFF)) ^ (this.crc >> 8); } this.crc ^= CrcSeed; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs new file mode 100644 index 000000000..5f62b13c7 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs @@ -0,0 +1,35 @@ +// 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 DeflateThrowHelper + { + [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."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNull(string name) => throw new ArgumentNullException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs new file mode 100644 index 000000000..6c4ea44d1 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs @@ -0,0 +1,295 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This class compresses input with the deflate algorithm described in RFC 1951. + /// It has several compression levels and three different strategies described below. + /// + internal sealed class Deflater : IDisposable + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BestCompression = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BestSpeed = 1; + + /// + /// The default compression level. + /// + public const int DefaultCompression = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + 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; + + /// + /// Compression level. + /// + private int level; + + /// + /// The current state. + /// + private int state; + + private DeflaterEngine engine; + private bool isDisposed; + + 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)); + } + + // TODO: Possibly provide DeflateStrategy as an option. + this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); + + this.SetLevel(level); + this.Reset(); + } + + /// + /// Compression Level as an enum for safer use + /// + public enum CompressionLevel + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + BestCompression = Deflater.BestCompression, + + /// + /// The worst but fastest compression level. + /// + BestSpeed = Deflater.BestSpeed, + + /// + /// The default compression level. + /// + DefaultCompression = Deflater.DefaultCompression, + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + 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 + } + + /// + /// Gets a value indicating whetherthe stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; + + /// + /// 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() + { + this.state = BusyState; + this.engine.Pending.Reset(); + this.engine.Reset(); + } + + /// + /// Flushes the current input block. Further calls to Deflate() will + /// produce enough output to inflate everything in the current input + /// block. It is used by DeflaterOutputStream to implement Flush(). + /// + [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 + /// be called to force all bytes to be flushed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Finish() => this.state |= IsFlushing | IsFinishing; + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// 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 finished or if previous input is still pending. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void SetInput(byte[] input, int offset, int count) + { + if ((this.state & IsFinishing) != 0) + { + DeflateThrowHelper.ThrowAlreadyFinished(); + } + + this.engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DefaultCompression) + { + level = 6; + } + else if (level < NoCompression || level > BestCompression) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + if (this.level != level) + { + this.level = level; + this.engine.SetLevel(level); + } + } + + /// + /// 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. + /// + /// The number of compressed bytes added to the output, or 0 if either + /// or returns true or length is zero. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (this.state == ClosedState) + { + DeflateThrowHelper.ThrowAlreadyClosed(); + } + + while (true) + { + int count = this.engine.Pending.Flush(output, offset, length); + offset += count; + length -= count; + + if (length == 0 || this.state == FinishedState) + { + break; + } + + if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) + { + switch (this.state) + { + case BusyState: + // We need more input now + return origLength - length; + + 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 + ((-this.engine.Pending.BitCount) & 7); + while (neededbits > 0) + { + // Write a static tree block consisting solely of an EOF: + this.engine.Pending.WriteBits(2, 10); + neededbits -= 10; + } + } + + this.state = BusyState; + break; + + case FinishingState: + this.engine.Pending.AlignToByte(); + this.state = FinishedState; + break; + } + } + } + + return origLength - length; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.engine.Dispose(); + this.engine = null; + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs new file mode 100644 index 000000000..67e8c6900 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// This class contains constants used for deflation. + /// + public static class DeflaterConstants + { + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs new file mode 100644 index 000000000..0163eec0b --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs @@ -0,0 +1,859 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Strategies for deflater + /// + public enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + internal sealed unsafe class DeflaterEngine : IDisposable + { + private const int TooFar = 4096; + + // Hash index of string to be inserted + private int insertHashIndex; + + private int matchStart; + + // Length of best match + private int matchLen; + + // Set if previous match exists + private bool prevAvailable; + + private int blockStart; + + /// + /// Points to the current character in the window. + /// + private int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + private int lookahead; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[] inputBuf; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private readonly DeflateStrategy strategy; + private DeflaterHuffman huffman; + private bool isDisposed; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private IMemoryOwner headMemoryOwner; + private MemoryHandle headMemoryHandle; + private readonly Memory head; + private readonly short* pinnedHeadPointer; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xFFFF. + /// + private IMemoryOwner prevMemoryOwner; + private MemoryHandle prevMemoryHandle; + private readonly Memory prev; + private readonly short* pinnedPrevPointer; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private IManagedByteBuffer windowMemoryOwner; + private MemoryHandle windowMemoryHandle; + private readonly byte[] window; + private readonly byte* pinnedWindowPointer; + + private int maxChain; + private int maxLazy; + private int niceLength; + private int goodLength; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The deflate strategy to use. + public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) + { + this.huffman = new DeflaterHuffman(memoryAllocator); + this.Pending = this.huffman.Pending; + this.strategy = strategy; + + // Create pinned pointers to the various buffers to allow indexing + // without bounds checks. + this.windowMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE); + this.window = this.windowMemoryOwner.Array; + this.windowMemoryHandle = this.windowMemoryOwner.Memory.Pin(); + this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; + + this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); + this.head = this.headMemoryOwner.Memory; + this.headMemoryHandle = this.headMemoryOwner.Memory.Pin(); + this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; + + this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE); + this.prev = this.prevMemoryOwner.Memory; + this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin(); + this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + this.blockStart = this.strstart = 1; + } + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; } + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress = false; + do + { + this.FillWindow(); + bool canFlush = flush && (this.inputOff == this.inputEnd); + + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + progress = this.DeflateStored(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_FAST: + progress = this.DeflateFast(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_SLOW: + progress = this.DeflateSlow(canFlush, finish); + break; + + default: + DeflateThrowHelper.ThrowUnknownCompression(); + break; + } + } + while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[] buffer, int offset, int count) + { + if (buffer is null) + { + DeflateThrowHelper.ThrowNull(nameof(buffer)); + } + + if (offset < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); + } + + if (count < 0) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + if (this.inputOff < this.inputEnd) + { + DeflateThrowHelper.ThrowNotProcessed(); + } + + int end = offset + count; + + // We want to throw an ArgumentOutOfRangeException early. + // The check is very tricky: it also handles integer wrap around. + if ((offset > end) || (end > buffer.Length)) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(count)); + } + + this.inputBuf = buffer; + this.inputOff = offset; + this.inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + [MethodImpl(InliningOptions.ShortMethod)] + public bool NeedsInput() => this.inputEnd == this.inputOff; + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.huffman.Reset(); + this.blockStart = this.strstart = 1; + this.lookahead = 0; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + this.head.Span.Slice(0, DeflaterConstants.HASH_SIZE).Clear(); + this.prev.Span.Slice(0, DeflaterConstants.WSIZE).Clear(); + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ((level < 0) || (level > 9)) + { + DeflateThrowHelper.ThrowOutOfRange(nameof(level)); + } + + this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; + this.maxLazy = DeflaterConstants.MAX_LAZY[level]; + this.niceLength = DeflaterConstants.NICE_LENGTH[level]; + this.maxChain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction) + { + switch (this.compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + if (this.strstart > this.blockStart) + { + this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.UpdateHash(); + break; + + case DeflaterConstants.DEFLATE_FAST: + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + break; + + case DeflaterConstants.DEFLATE_SLOW: + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); + } + + if (this.strstart > this.blockStart) + { + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false); + this.blockStart = this.strstart; + } + + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + break; + } + + this.compressionFunction = DeflaterConstants.COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + { + this.SlideWindow(); + } + + // If there is not enough lookahead, but still some input left, read in the input. + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd) + { + int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart; + + if (more > this.inputEnd - this.inputOff) + { + more = this.inputEnd - this.inputOff; + } + + Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more); + + this.inputOff += more; + this.lookahead += more; + } + + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.UpdateHash(); + } + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.huffman.Dispose(); + + this.windowMemoryHandle.Dispose(); + this.windowMemoryOwner.Dispose(); + + this.headMemoryHandle.Dispose(); + this.headMemoryOwner.Dispose(); + + this.prevMemoryHandle.Dispose(); + this.prevMemoryOwner.Dispose(); + + this.windowMemoryOwner = null; + this.headMemoryOwner = null; + this.prevMemoryOwner = null; + this.huffman = null; + + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void UpdateHash() + { + byte* pinned = this.pinnedWindowPointer; + this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + [MethodImpl(InliningOptions.ShortMethod)] + private int InsertString() + { + short match; + int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + + short* pinnedHead = this.pinnedHeadPointer; + this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash]; + pinnedHead[hash] = unchecked((short)this.strstart); + this.insertHashIndex = hash; + return match & 0xFFFF; + } + + private void SlideWindow() + { + Unsafe.CopyBlockUnaligned(ref this.window[0], ref this.window[DeflaterConstants.WSIZE], DeflaterConstants.WSIZE); + this.matchStart -= DeflaterConstants.WSIZE; + this.strstart -= DeflaterConstants.WSIZE; + this.blockStart -= DeflaterConstants.WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + short* pinnedHead = this.pinnedHeadPointer; + for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) + { + int m = pinnedHead[i] & 0xFFFF; + pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + + // Slide the prev table. + short* pinnedPrev = this.pinnedPrevPointer; + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + int m = pinnedPrev[i] & 0xFFFF; + pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + } + + /// + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// + /// Preconditions: + /// + /// strstart + DeflaterConstants.MAX_MATCH <= window.length. + /// + /// + /// The current match. + /// True if a match greater than the minimum length is found + private bool FindLongestMatch(int curMatch) + { + int match; + int scan = this.strstart; + + // scanMax is the highest position that we can look at + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; + int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + + int chainLength = this.maxChain; + int niceLength = Math.Min(this.niceLength, this.lookahead); + + int matchStrt = this.matchStart; + this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); + int matchLength = this.matchLen; + + if (scan + matchLength > scanMax) + { + return false; + } + + byte* pinnedWindow = this.pinnedWindowPointer; + int scanStart = this.strstart; + byte scanEnd1 = pinnedWindow[scan + matchLength - 1]; + byte scanEnd = pinnedWindow[scan + matchLength]; + + // Do not waste too much time if we already have a good match: + if (matchLength >= this.goodLength) + { + chainLength >>= 2; + } + + short* pinnedPrev = this.pinnedPrevPointer; + do + { + match = curMatch; + scan = scanStart; + + if (pinnedWindow[match + matchLength] != scanEnd + || pinnedWindow[match + matchLength - 1] != scanEnd1 + || pinnedWindow[match] != pinnedWindow[scan] + || pinnedWindow[++match] != pinnedWindow[++scan]) + { + continue; + } + + // scan is set to strstart+1 and the comparison passed, so + // scanMax - scan is the maximum number of bytes we can compare. + // below we compare 8 bytes at a time, so first we compare + // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 + // n & (8 - 1) == n % 8. + switch ((scanMax - scan) & 7) + { + case 1: + if (pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 2: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 3: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 4: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 5: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 6: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + + case 7: + if (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]) + { + break; + } + + break; + } + + if (pinnedWindow[scan] == pinnedWindow[match]) + { + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart + 258 unless lookahead is + // exhausted first. + do + { + if (scan == scanMax) + { + ++scan; // advance to first position not matched + ++match; + + break; + } + } + while (pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match] + && pinnedWindow[++scan] == pinnedWindow[++match]); + } + + if (scan - scanStart > matchLength) + { + matchStrt = curMatch; + matchLength = scan - scanStart; + + if (matchLength >= niceLength) + { + break; + } + + scanEnd1 = pinnedWindow[scan - 1]; + scanEnd = pinnedWindow[scan]; + } + } + while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); + + this.matchStart = matchStrt; + this.matchLen = matchLength; + return matchLength >= DeflaterConstants.MIN_MATCH; + } + + private bool DeflateStored(bool flush, bool finish) + { + if (!flush && (this.lookahead == 0)) + { + return false; + } + + this.strstart += this.lookahead; + this.lookahead = 0; + + int storedLength = this.strstart - this.blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + flush) + { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) + { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + + this.huffman.FlushStoredBlock(this.window, this.blockStart, storedLength, lastBlock); + this.blockStart += storedLength; + return !(lastBlock || storedLength == 0); + } + + return true; + } + + private bool DeflateFast(bool flush, bool finish) + { + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + // We are flushing everything + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + if (this.strstart > (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) + { + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); + } + + int hashHead; + if (this.lookahead >= DeflaterConstants.MIN_MATCH && + (hashHead = this.InsertString()) != 0 && + this.strategy != DeflateStrategy.HuffmanOnly && + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); + + this.lookahead -= this.matchLen; + if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) + { + while (--this.matchLen > 0) + { + ++this.strstart; + this.InsertString(); + } + + ++this.strstart; + } + else + { + this.strstart += this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) + { + this.UpdateHash(); + } + } + + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + if (!full) + { + continue; + } + } + else + { + // No match found + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); + ++this.strstart; + --this.lookahead; + } + + if (this.huffman.IsFull()) + { + bool lastBlock = finish && (this.lookahead == 0); + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, lastBlock); + this.blockStart = this.strstart; + return !lastBlock; + } + } + + return true; + } + + private bool DeflateSlow(bool flush, bool finish) + { + if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (this.lookahead == 0) + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = false; + + // We are flushing everything + this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); + this.blockStart = this.strstart; + return false; + } + + if (this.strstart >= (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) + { + // slide window, as FindLongestMatch needs this. + // This should only happen when flushing and the window + // is almost full. + this.SlideWindow(); + } + + int prevMatch = this.matchStart; + int prevLen = this.matchLen; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + int hashHead = this.InsertString(); + + if (this.strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + this.strstart - hashHead <= DeflaterConstants.MAX_DIST && + this.FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + // Discard match if too small and too far away + if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) + { + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) + { + this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do + { + this.strstart++; + this.lookahead--; + if (this.lookahead >= DeflaterConstants.MIN_MATCH) + { + this.InsertString(); + } + } + while (--prevLen > 0); + + this.strstart++; + this.lookahead--; + this.prevAvailable = false; + this.matchLen = DeflaterConstants.MIN_MATCH - 1; + } + else + { + if (this.prevAvailable) + { + this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); + } + + this.prevAvailable = true; + this.strstart++; + this.lookahead--; + } + + if (this.huffman.IsFull()) + { + int len = this.strstart - this.blockStart; + if (this.prevAvailable) + { + len--; + } + + bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable; + this.huffman.FlushBlock(this.window, this.blockStart, len, lastBlock); + this.blockStart += len; + return !lastBlock; + } + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs new file mode 100644 index 000000000..96ff6b657 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -0,0 +1,949 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Performs Deflate Huffman encoding. + /// + internal sealed unsafe class DeflaterHuffman : IDisposable + { + private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + + // The number of literal codes. + private const int LiteralNumber = 286; + + // Number of distance codes + private const int DistanceNumber = 30; + + // Number of codes used to transfer bit lengths + private const int BitLengthNumber = 19; + + // Repeat previous bit length 3-6 times (2 bits of repeat count) + private const int Repeat3To6 = 16; + + // Repeat a zero length 3-10 times (3 bits of repeat count) + private const int Repeat3To10 = 17; + + // Repeat a zero length 11-138 times (7 bits of repeat count) + private const int Repeat11To138 = 18; + + private const int EofSymbol = 256; + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit length codes. + private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; + + private static readonly short[] StaticLCodes; + private static readonly byte[] StaticLLength; + private static readonly short[] StaticDCodes; + private static readonly byte[] StaticDLength; + + private Tree literalTree; + private Tree distTree; + private Tree blTree; + + // Buffer for distances + private readonly IMemoryOwner distanceManagedBuffer; + private readonly short* pinnedDistanceBuffer; + private MemoryHandle distanceBufferHandle; + + private readonly IMemoryOwner literalManagedBuffer; + private readonly short* pinnedLiteralBuffer; + private MemoryHandle literalBufferHandle; + + private int lastLiteral; + private int extraBits; + private bool isDisposed; + + // TODO: These should be pre-generated array/readonlyspans. + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + StaticLCodes = new short[LiteralNumber]; + StaticLLength = new byte[LiteralNumber]; + + int i = 0; + while (i < 144) + { + StaticLCodes[i] = BitReverse((0x030 + i) << 8); + StaticLLength[i++] = 8; + } + + while (i < 256) + { + StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + StaticLLength[i++] = 9; + } + + while (i < 280) + { + StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + StaticLLength[i++] = 7; + } + + while (i < LiteralNumber) + { + StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + StaticLLength[i++] = 8; + } + + // Distance codes + StaticDCodes = new short[DistanceNumber]; + StaticDLength = new byte[DistanceNumber]; + for (i = 0; i < DistanceNumber; i++) + { + StaticDCodes[i] = BitReverse(i << 11); + StaticDLength[i] = 5; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterHuffman(MemoryAllocator memoryAllocator) + { + this.Pending = new DeflaterPendingBuffer(memoryAllocator); + + this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15); + this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15); + this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7); + + this.distanceManagedBuffer = memoryAllocator.Allocate(BufferSize); + this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin(); + this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; + + this.literalManagedBuffer = memoryAllocator.Allocate(BufferSize); + this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin(); + this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; + } + + /// + /// Gets the pending buffer to use. + /// + public DeflaterPendingBuffer Pending { get; private set; } + + /// + /// Reset internal state + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.lastLiteral = 0; + this.extraBits = 0; + this.literalTree.Reset(); + this.distTree.Reset(); + this.blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + this.blTree.BuildCodes(); + this.literalTree.BuildCodes(); + this.distTree.BuildCodes(); + this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); + this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); + this.Pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) + { + this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); + } + + this.literalTree.WriteTree(this.Pending, this.blTree); + this.distTree.WriteTree(this.Pending, this.blTree); + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + DeflaterPendingBuffer pendingBuffer = this.Pending; + short* pinnedDistance = this.pinnedDistanceBuffer; + short* pinnedLiteral = this.pinnedLiteralBuffer; + + for (int i = 0; i < this.lastLiteral; i++) + { + int litlen = pinnedLiteral[i] & 0xFF; + int dist = pinnedDistance[i]; + if (dist-- != 0) + { + int lc = Lcode(litlen); + this.literalTree.WriteSymbol(pendingBuffer, lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) + { + this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + this.distTree.WriteSymbol(pendingBuffer, dc); + + bits = (dc >> 1) - 1; + if (bits > 0) + { + this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + this.literalTree.WriteSymbol(pendingBuffer, litlen); + } + } + + this.literalTree.WriteSymbol(pendingBuffer, EofSymbol); + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + [MethodImpl(InliningOptions.ShortMethod)] + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + this.Pending.AlignToByte(); + this.Pending.WriteShort(storedLength); + this.Pending.WriteShort(~storedLength); + this.Pending.WriteBlock(stored, storedOffset, storedLength); + this.Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + this.literalTree.Frequencies[EofSymbol]++; + + // Build trees + this.literalTree.BuildTree(); + this.distTree.BuildTree(); + + // Calculate bitlen frequency + this.literalTree.CalcBLFreq(this.blTree); + this.distTree.CalcBLFreq(this.blTree); + + // Build bitlen tree + this.blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) + { + if (this.blTree.Length[BitLengthOrder[i]] > 0) + { + blTreeCodes = i + 1; + } + } + + int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() + + this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() + + this.extraBits; + + int static_len = this.extraBits; + ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); + for (int i = 0; i < LiteralNumber; i++) + { + static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); + } + + ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); + for (int i = 0; i < DistanceNumber; i++) + { + static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i); + } + + if (opt_len >= static_len) + { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) + { + // Store Block + this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } + else if (opt_len == static_len) + { + // Encode with static tree + this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength); + this.distTree.SetStaticCodes(StaticDCodes, StaticDLength); + this.CompressBlock(); + this.Reset(); + } + else + { + // Encode with dynamic tree + this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + this.SendAllTrees(blTreeCodes); + this.CompressBlock(); + this.Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsFull() => this.lastLiteral >= BufferSize; + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool TallyLit(int literal) + { + this.pinnedDistanceBuffer[this.lastLiteral] = 0; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; + this.literalTree.Frequencies[literal]++; + return this.IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + [MethodImpl(InliningOptions.ShortMethod)] + public bool TallyDist(int distance, int length) + { + this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; + this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + this.literalTree.Frequencies[lc]++; + if (lc >= 265 && lc < 285) + { + this.extraBits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + this.distTree.Frequencies[dc]++; + if (dc >= 4) + { + this.extraBits += (dc >> 1) - 1; + } + + return this.IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + [MethodImpl(InliningOptions.ShortMethod)] + public static short BitReverse(int toReverse) + { + return (short)(Bit4Reverse[toReverse & 0xF] << 12 + | Bit4Reverse[(toReverse >> 4) & 0xF] << 8 + | Bit4Reverse[(toReverse >> 8) & 0xF] << 4 + | Bit4Reverse[toReverse >> 12]); + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.Pending.Dispose(); + this.distanceBufferHandle.Dispose(); + this.distanceManagedBuffer.Dispose(); + this.literalBufferHandle.Dispose(); + this.literalManagedBuffer.Dispose(); + + this.literalTree.Dispose(); + this.blTree.Dispose(); + this.distTree.Dispose(); + + this.Pending = null; + this.literalTree = null; + this.blTree = null; + this.distTree = null; + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } + + return code + length; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + + return code + distance; + } + + private sealed class Tree : IDisposable + { + private readonly int minNumCodes; + private readonly int[] bitLengthCounts; + private readonly int maxLength; + private bool isDisposed; + + private readonly int elementCount; + + private readonly MemoryAllocator memoryAllocator; + + private IMemoryOwner codesMemoryOwner; + private MemoryHandle codesMemoryHandle; + private readonly short* codes; + + private IMemoryOwner frequenciesMemoryOwner; + private MemoryHandle frequenciesMemoryHandle; + + private IManagedByteBuffer lengthsMemoryOwner; + private MemoryHandle lengthsMemoryHandle; + + public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) + { + this.memoryAllocator = memoryAllocator; + this.elementCount = elements; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + + this.frequenciesMemoryOwner = memoryAllocator.Allocate(elements); + this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin(); + this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer; + + this.lengthsMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(elements); + this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin(); + this.Length = (byte*)this.lengthsMemoryHandle.Pointer; + + this.codesMemoryOwner = memoryAllocator.Allocate(elements); + this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin(); + this.codes = (short*)this.codesMemoryHandle.Pointer; + + // Maxes out at 15. + this.bitLengthCounts = new int[maxLength]; + } + + public int NumCodes { get; private set; } + + public short* Frequencies { get; } + + public byte* Length { get; } + + /// + /// Resets the internal state of the tree + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() + { + this.frequenciesMemoryOwner.Memory.Span.Clear(); + this.lengthsMemoryOwner.Memory.Span.Clear(); + this.codesMemoryOwner.Memory.Span.Clear(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) + => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + [MethodImpl(InliningOptions.ShortMethod)] + public void SetStaticCodes(ReadOnlySpan staticCodes, ReadOnlySpan staticLengths) + { + staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); + staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + // Maxes out at 15 * 4 + Span nextCode = stackalloc int[this.maxLength]; + ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + + int code = 0; + for (int bits = 0; bits < this.maxLength; bits++) + { + Unsafe.Add(ref nextCodeRef, bits) = code; + code += Unsafe.Add(ref bitLengthCountsRef, bits) << (15 - bits); + } + + for (int i = 0; i < this.NumCodes; i++) + { + int bits = this.Length[i]; + if (bits > 0) + { + this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, bits - 1)); + Unsafe.Add(ref nextCodeRef, bits - 1) += 1 << (16 - bits); + } + } + } + + public void BuildTree() + { + int numSymbols = this.elementCount; + + // heap is a priority queue, sorted by frequency, least frequent + // nodes first. The heap is a binary tree, with the property, that + // the parent node is smaller than both child nodes. This assures + // that the smallest node is the first parent. + // + // The binary tree is encoded in an array: 0 is root node and + // the nodes 2*n+1, 2*n+2 are the child nodes of node n. + // Maxes out at 286 * 4 so too large for the stack. + using (IMemoryOwner heapMemoryOwner = this.memoryAllocator.Allocate(numSymbols)) + { + ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); + + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) + { + int freq = this.Frequencies[n]; + if (freq != 0) + { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) >> 1)] > freq) + { + Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos); + pos = ppos; + } + + Unsafe.Add(ref heapRef, pos) = n; + + maxCode = n; + } + } + + // We could encode a single literal with 0 bits but then we + // don't see the literals. Therefore we force at least two + // literals to avoid this case. We don't care about order in + // this case, both literals get a 1 bit code. + while (heapLen < 2) + { + Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0; + } + + this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); + + int numLeafs = heapLen; + int childrenLength = (4 * heapLen) - 2; + using (IMemoryOwner childrenMemoryOwner = this.memoryAllocator.Allocate(childrenLength)) + using (IMemoryOwner valuesMemoryOwner = this.memoryAllocator.Allocate((2 * heapLen) - 1)) + { + ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span); + ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span); + int numNodes = numLeafs; + + for (int i = 0; i < heapLen; i++) + { + int node = Unsafe.Add(ref heapRef, i); + int i2 = 2 * i; + Unsafe.Add(ref childrenRef, i2) = node; + Unsafe.Add(ref childrenRef, i2 + 1) = -1; + Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8; + Unsafe.Add(ref heapRef, i) = i; + } + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + do + { + int first = Unsafe.Add(ref heapRef, 0); + int last = Unsafe.Add(ref heapRef, --heapLen); + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) + { + path++; + } + + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); + ppos = path; + path = (path * 2) + 1; + } + + // Now propagate the last element down along path. Normally + // it shouldn't go too deep. + int lastVal = Unsafe.Add(ref valuesRef, last); + while ((path = ppos) > 0 + && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) + { + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + } + + Unsafe.Add(ref heapRef, path) = last; + + int second = Unsafe.Add(ref heapRef, 0); + + // Create a new node father of first and second + last = numNodes++; + Unsafe.Add(ref childrenRef, 2 * last) = first; + Unsafe.Add(ref childrenRef, (2 * last) + 1) = second; + int mindepth = Math.Min(Unsafe.Add(ref valuesRef, first) & 0xFF, Unsafe.Add(ref valuesRef, second) & 0xFF); + Unsafe.Add(ref valuesRef, last) = lastVal = Unsafe.Add(ref valuesRef, first) + Unsafe.Add(ref valuesRef, second) - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen + && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1))) + { + path++; + } + + Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path); + ppos = path; + path = (ppos * 2) + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal) + { + Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos); + } + + Unsafe.Add(ref heapRef, path) = last; + } + while (heapLen > 1); + + if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) + { + DeflateThrowHelper.ThrowHeapViolated(); + } + + this.BuildLength(childrenMemoryOwner.Memory.Span); + } + } + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + [MethodImpl(InliningOptions.ShortMethod)] + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < this.elementCount; i++) + { + len += this.Frequencies[i] * this.Length[i]; + } + + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code + + int i = 0; + while (i < this.NumCodes) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + blTree.Frequencies[nextlen]++; + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + blTree.Frequencies[curLen] += (short)count; + } + else if (curLen != 0) + { + blTree.Frequencies[Repeat3To6]++; + } + else if (count <= 10) + { + blTree.Frequencies[Repeat3To10]++; + } + else + { + blTree.Frequencies[Repeat11To138]++; + } + } + } + + /// + /// Write the tree values. + /// + /// The pending buffer. + /// The tree to write. + public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) + { + int maxCount; // max repeat count + int minCount; // min repeat count + int count; // repeat count of the current code + int curLen = -1; // length of current code + + int i = 0; + while (i < this.NumCodes) + { + count = 1; + int nextlen = this.Length[i]; + if (nextlen == 0) + { + maxCount = 138; + minCount = 3; + } + else + { + maxCount = 6; + minCount = 3; + if (curLen != nextlen) + { + bitLengthTree.WriteSymbol(pendingBuffer, nextlen); + count = 0; + } + } + + curLen = nextlen; + i++; + + while (i < this.NumCodes && curLen == this.Length[i]) + { + i++; + if (++count >= maxCount) + { + break; + } + } + + if (count < minCount) + { + while (count-- > 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, curLen); + } + } + else if (curLen != 0) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); + pendingBuffer.WriteBits(count - 3, 2); + } + else if (count <= 10) + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); + pendingBuffer.WriteBits(count - 3, 3); + } + else + { + bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); + pendingBuffer.WriteBits(count - 11, 7); + } + } + } + + private void BuildLength(ReadOnlySpan children) + { + byte* lengthPtr = this.Length; + ref int childrenRef = ref MemoryMarshal.GetReference(children); + ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts); + + int maxLen = this.maxLength; + int numNodes = children.Length >> 1; + int numLeafs = (numNodes + 1) >> 1; + int overflow = 0; + + Array.Clear(this.bitLengthCounts, 0, maxLen); + + // First calculate optimal bit lengths + using (IMemoryOwner lengthsMemoryOwner = this.memoryAllocator.Allocate(numNodes, AllocationOptions.Clean)) + { + ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); + + for (int i = numNodes - 1; i >= 0; i--) + { + if (children[(2 * i) + 1] != -1) + { + int bitLength = Unsafe.Add(ref lengthsRef, i) + 1; + if (bitLength > maxLen) + { + bitLength = maxLen; + overflow++; + } + + Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, 2 * i)) = Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, (2 * i) + 1)) = bitLength; + } + else + { + // A leaf node + int bitLength = Unsafe.Add(ref lengthsRef, i); + Unsafe.Add(ref bitLengthCountsRef, bitLength - 1)++; + lengthPtr[Unsafe.Add(ref childrenRef, 2 * i)] = (byte)Unsafe.Add(ref lengthsRef, i); + } + } + } + + if (overflow == 0) + { + return; + } + + int incrBitLen = maxLen - 1; + do + { + // Find the first bit length which could increase: + while (Unsafe.Add(ref bitLengthCountsRef, --incrBitLen) == 0) + { + } + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do + { + Unsafe.Add(ref bitLengthCountsRef, incrBitLen)--; + Unsafe.Add(ref bitLengthCountsRef, ++incrBitLen)++; + overflow -= 1 << (maxLen - 1 - incrBitLen); + } + while (overflow > 0 && incrBitLen < maxLen - 1); + } + while (overflow > 0); + + // We may have overshot above. Move some nodes from maxLength to + // maxLength-1 in that case. + Unsafe.Add(ref bitLengthCountsRef, maxLen - 1) += overflow; + Unsafe.Add(ref bitLengthCountsRef, maxLen - 2) -= overflow; + + // Now recompute all bit lengths, scanning in increasing + // frequency. It is simpler to reconstruct all lengths instead of + // fixing only the wrong ones. This idea is taken from 'ar' + // written by Haruhiko Okumura. + // + // The nodes were inserted with decreasing frequency into the childs + // array. + int nodeIndex = 2 * numLeafs; + for (int bits = maxLen; bits != 0; bits--) + { + int n = Unsafe.Add(ref bitLengthCountsRef, bits - 1); + while (n > 0) + { + int childIndex = 2 * Unsafe.Add(ref childrenRef, nodeIndex++); + if (Unsafe.Add(ref childrenRef, childIndex + 1) == -1) + { + // We found another leaf + lengthPtr[Unsafe.Add(ref childrenRef, childIndex)] = (byte)bits; + n--; + } + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.frequenciesMemoryHandle.Dispose(); + this.frequenciesMemoryOwner.Dispose(); + + this.lengthsMemoryHandle.Dispose(); + this.lengthsMemoryOwner.Dispose(); + + this.codesMemoryHandle.Dispose(); + this.codesMemoryOwner.Dispose(); + + this.frequenciesMemoryOwner = null; + this.lengthsMemoryOwner = null; + this.codesMemoryOwner = null; + + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs new file mode 100644 index 000000000..9eeb12cb0 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating. + /// + internal sealed class DeflaterOutputStream : Stream + { + private const int BufferLength = 512; + private IManagedByteBuffer memoryOwner; + private readonly byte[] buffer; + private Deflater deflater; + private readonly Stream rawStream; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The output stream where deflated output is written. + /// The compression level. + public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel) + { + this.rawStream = rawStream; + this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength); + this.buffer = this.memoryOwner.Array; + this.deflater = new Deflater(memoryAllocator, compressionLevel); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => this.rawStream.CanWrite; + + /// + public override long Length => this.rawStream.Length; + + /// + public override long Position + { + get + { + return this.rawStream.Position; + } + + set + { + throw new NotSupportedException(); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override int ReadByte() => throw new NotSupportedException(); + + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override void Flush() + { + this.deflater.Flush(); + this.Deflate(true); + this.rawStream.Flush(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + this.deflater.SetInput(buffer, offset, count); + this.Deflate(); + } + + private void Deflate() => this.Deflate(false); + + private void Deflate(bool flushing) + { + while (flushing || !this.deflater.IsNeedingInput) + { + int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength); + + if (deflateCount <= 0) + { + break; + } + + this.rawStream.Write(this.buffer, 0, deflateCount); + } + + if (!this.deflater.IsNeedingInput) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + } + + private void Finish() + { + this.deflater.Finish(); + while (!this.deflater.IsFinished) + { + int len = this.deflater.Deflate(this.buffer, 0, BufferLength); + if (len <= 0) + { + break; + } + + this.rawStream.Write(this.buffer, 0, len); + } + + if (!this.deflater.IsFinished) + { + DeflateThrowHelper.ThrowNoDeflate(); + } + + this.rawStream.Flush(); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Finish(); + this.deflater.Dispose(); + this.memoryOwner.Dispose(); + } + + this.deflater = null; + this.memoryOwner = null; + this.isDisposed = true; + base.Dispose(disposing); + } + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs new file mode 100644 index 000000000..a5f00f03c --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.Png.Zlib +{ + /// + /// Stores pending data for writing data to the Deflater. + /// + internal sealed unsafe class DeflaterPendingBuffer : IDisposable + { + private readonly byte[] buffer; + private readonly byte* pinnedBuffer; + private IManagedByteBuffer bufferMemoryOwner; + private MemoryHandle bufferMemoryHandle; + + private int start; + private int end; + private uint bits; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) + { + this.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE); + this.buffer = this.bufferMemoryOwner.Array; + this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin(); + this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer; + } + + /// + /// Gets the number of bits written to the buffer. + /// + public int BitCount { get; private set; } + + /// + /// Gets a value indicating whether indicates the buffer has been flushed. + /// + public bool IsFlushed => this.end == 0; + + /// + /// Clear internal state/buffers. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Reset() => this.start = this.end = this.BitCount = 0; + + /// + /// Write a short value to buffer LSB first. + /// + /// The value to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShort(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)value); + pinned[this.end++] = unchecked((byte)(value >> 8)); + } + + /// + /// Write a block of data to the internal buffer. + /// + /// The data to write. + /// The offset of first byte to write. + /// The number of bytes to write. + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBlock(byte[] block, int offset, int length) + { + Unsafe.CopyBlockUnaligned(ref this.buffer[this.end], ref block[offset], unchecked((uint)length)); + this.end += length; + } + + /// + /// Aligns internal buffer on a byte boundary. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void AlignToByte() + { + if (this.BitCount > 0) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + if (this.BitCount > 8) + { + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + } + } + + this.bits = 0; + this.BitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteBits(int b, int count) + { + this.bits |= (uint)(b << this.BitCount); + this.BitCount += count; + if (this.BitCount >= 16) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)this.bits); + pinned[this.end++] = unchecked((byte)(this.bits >> 8)); + this.bits >>= 16; + this.BitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// The value to write + [MethodImpl(InliningOptions.ShortMethod)] + public void WriteShortMSB(int value) + { + byte* pinned = this.pinnedBuffer; + pinned[this.end++] = unchecked((byte)(value >> 8)); + pinned[this.end++] = unchecked((byte)value); + } + + /// + /// Flushes the pending buffer into the given output array. + /// If the output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (this.BitCount >= 8) + { + this.pinnedBuffer[this.end++] = unchecked((byte)this.bits); + this.bits >>= 8; + this.BitCount -= 8; + } + + if (length > this.end - this.start) + { + length = this.end - this.start; + + Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); + this.start = 0; + this.end = 0; + } + else + { + Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length)); + this.start += length; + } + + return length; + } + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.bufferMemoryHandle.Dispose(); + this.bufferMemoryOwner.Dispose(); + this.bufferMemoryOwner = null; + this.isDisposed = true; + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/README.md b/src/ImageSharp/Formats/Png/Zlib/README.md index c297a91d5..59f75d05f 100644 --- a/src/ImageSharp/Formats/Png/Zlib/README.md +++ b/src/ImageSharp/Formats/Png/Zlib/README.md @@ -1,2 +1,5 @@ -Adler32.cs and Crc32.cs have been copied from -https://github.com/ygrenier/SharpZipLib.Portable +Deflatestream implementation adapted from + +https://github.com/icsharpcode/SharpZipLib + +LIcensed under MIT diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs index 8e0bac938..3c52d306f 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -1,9 +1,9 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.IO; -using System.IO.Compression; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Png.Zlib { @@ -38,14 +38,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib /// /// The stream responsible for compressing the input stream. /// - private System.IO.Compression.DeflateStream deflateStream; + // private DeflateStream deflateStream; + private DeflaterOutputStream deflateStream; /// /// Initializes a new instance of the class. /// + /// The memory allocator to use for buffer allocations. /// The stream to compress. /// The compression level. - public ZlibDeflateStream(Stream stream, int compressionLevel) + public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, int compressionLevel) { this.rawStream = stream; @@ -60,7 +62,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 @@ -78,29 +80,17 @@ 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. - CompressionLevel level = CompressionLevel.Optimal; - - if (compressionLevel >= 1 && compressionLevel <= 5) - { - level = CompressionLevel.Fastest; - } - else if (compressionLevel == 0) - { - level = CompressionLevel.NoCompression; - } - - this.deflateStream = new System.IO.Compression.DeflateStream(this.rawStream, level, true); + this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel); } /// @@ -110,41 +100,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib public override bool CanSeek => false; /// - public override bool CanWrite => true; + public override bool CanWrite => this.rawStream.CanWrite; /// - public override long Length => throw new NotSupportedException(); + public override long Length => this.rawStream.Length; /// public override long Position { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); + get + { + return this.rawStream.Position; + } + + set + { + throw new NotSupportedException(); + } } /// - public override void Flush() - { - this.deflateStream?.Flush(); - } + public override void Flush() => this.deflateStream.Flush(); /// - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// public override void Write(byte[] buffer, int offset, int count) @@ -164,17 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib if (disposing) { // dispose managed resources - if (this.deflateStream != null) - { - this.deflateStream.Dispose(); - this.deflateStream = null; - } - else - { - // Hack: empty input? - this.rawStream.WriteByte(3); - this.rawStream.WriteByte(0); - } + this.deflateStream.Dispose(); // Add the crc uint crc = (uint)this.adler32.Value; @@ -184,11 +159,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.rawStream.WriteByte((byte)(crc & 0xFF)); } - base.Dispose(disposing); + this.deflateStream = null; - // Call the appropriate methods to clean up - // unmanaged resources here. - // Note disposing is done. + base.Dispose(disposing); this.isDisposed = true; } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs index 157dadd2c..7bd1b8044 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.Drawing.Imaging; using System.IO; using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests; using SDImage = System.Drawing.Image; @@ -56,8 +57,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - this.bmpCore.SaveAsPng(memoryStream); + var encoder = new PngEncoder { FilterMethod = PngFilterMethod.None }; + this.bmpCore.SaveAsPng(memoryStream, encoder); } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 2584391bb..8a0cdbfba 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png /// public static readonly TheoryData CompressionLevels = new TheoryData { - 1, 2, 3, 4, 5, 6, 7, 8, 9 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; public static readonly TheoryData PaletteSizes = new TheoryData