Browse Source

Merge pull request #1054 from SixLabors/js/managed-zlib

Fix Png Encoder Compression
pull/1059/head
James Jackson-South 6 years ago
committed by GitHub
parent
commit
ec4eca1074
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  2. 11
      src/ImageSharp/Formats/Png/Zlib/Adler32.cs
  3. 6
      src/ImageSharp/Formats/Png/Zlib/Crc32.cs
  4. 35
      src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs
  5. 295
      src/ImageSharp/Formats/Png/Zlib/Deflater.cs
  6. 151
      src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs
  7. 859
      src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
  8. 949
      src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
  9. 153
      src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs
  10. 179
      src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
  11. 7
      src/ImageSharp/Formats/Png/Zlib/README.md
  12. 83
      src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs
  13. 8
      tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs
  14. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

4
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)
{

11
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<byte> 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;
}
}
}
}

6
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;

35
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.");
}
}

295
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
{
/// <summary>
/// This class compresses input with the deflate algorithm described in RFC 1951.
/// It has several compression levels and three different strategies described below.
/// </summary>
internal sealed class Deflater : IDisposable
{
/// <summary>
/// The best and slowest compression level. This tries to find very
/// long and distant string repetitions.
/// </summary>
public const int BestCompression = 9;
/// <summary>
/// The worst but fastest compression level.
/// </summary>
public const int BestSpeed = 1;
/// <summary>
/// The default compression level.
/// </summary>
public const int DefaultCompression = -1;
/// <summary>
/// This level won't compress at all but output uncompressed blocks.
/// </summary>
public const int NoCompression = 0;
/// <summary>
/// The compression method. This is the only method supported so far.
/// There is no need to use this constant at all.
/// </summary>
public const int Deflated = 8;
/// <summary>
/// Compression level.
/// </summary>
private int level;
/// <summary>
/// The current state.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="Deflater"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="level">The compression level, a value between NoCompression and BestCompression.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">if level is out of range.</exception>
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();
}
/// <summary>
/// Compression Level as an enum for safer use
/// </summary>
public enum CompressionLevel
{
/// <summary>
/// The best and slowest compression level. This tries to find very
/// long and distant string repetitions.
/// </summary>
BestCompression = Deflater.BestCompression,
/// <summary>
/// The worst but fastest compression level.
/// </summary>
BestSpeed = Deflater.BestSpeed,
/// <summary>
/// The default compression level.
/// </summary>
DefaultCompression = Deflater.DefaultCompression,
/// <summary>
/// This level won't compress at all but output uncompressed blocks.
/// </summary>
NoCompression = Deflater.NoCompression,
/// <summary>
/// The compression method. This is the only method supported so far.
/// There is no need to use this constant at all.
/// </summary>
Deflated = Deflater.Deflated
}
/// <summary>
/// Gets a value indicating whetherthe stream was finished and no more output bytes
/// are available.
/// </summary>
public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed;
/// <summary>
/// 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.
/// </summary>
public bool IsNeedingInput => this.engine.NeedsInput();
/// <summary>
/// Resets the deflater. The deflater acts afterwards as if it was
/// just created with the same compression level and strategy as it
/// had before.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.state = BusyState;
this.engine.Pending.Reset();
this.engine.Reset();
}
/// <summary>
/// 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().
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Flush() => this.state |= IsFlushing;
/// <summary>
/// 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.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Finish() => this.state |= IsFlushing | IsFinishing;
/// <summary>
/// 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.
/// </summary>
/// <param name="input">The buffer containing the input data.</param>
/// <param name="offset">The start of the data.</param>
/// <param name="count">The number of data bytes of input.</param>
/// <exception cref="InvalidOperationException">
/// if the buffer was finished or if previous input is still pending.
/// </exception>
[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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="level">
/// the new compression level.
/// </param>
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);
}
}
/// <summary>
/// Deflates the current input block to the given array.
/// </summary>
/// <param name="output">Buffer to store the compressed data.</param>
/// <param name="offset">Offset into the output array.</param>
/// <param name="length">The maximum number of bytes that may be stored.</param>
/// <returns>
/// The number of compressed bytes added to the output, or 0 if either
/// <see cref="IsNeedingInput"/> or <see cref="IsFinished"/> returns true or length is zero.
/// </returns>
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;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.engine.Dispose();
this.engine = null;
this.isDisposed = true;
}
GC.SuppressFinalize(this);
}
}
}

151
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.
// <auto-generated/>
using System;
using System.Collections.Generic;
using System.Text;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
/// <summary>
/// This class contains constants used for deflation.
/// </summary>
public static class DeflaterConstants
{
/// <summary>
/// Set to true to enable debugging
/// </summary>
public const bool DEBUGGING = false;
/// <summary>
/// Written to Zip file to identify a stored block
/// </summary>
public const int STORED_BLOCK = 0;
/// <summary>
/// Identifies static tree in Zip file
/// </summary>
public const int STATIC_TREES = 1;
/// <summary>
/// Identifies dynamic tree in Zip file
/// </summary>
public const int DYN_TREES = 2;
/// <summary>
/// Header flag indicating a preset dictionary for deflation
/// </summary>
public const int PRESET_DICT = 0x20;
/// <summary>
/// Sets internal buffer sizes for Huffman encoding
/// </summary>
public const int DEFAULT_MEM_LEVEL = 8;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_MATCH = 258;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MIN_MATCH = 3;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_WBITS = 15;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int WSIZE = 1 << MAX_WBITS;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int WMASK = WSIZE - 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_SIZE = 1 << HASH_BITS;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_MASK = HASH_SIZE - 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8);
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5);
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_STORED = 0;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_FAST = 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_SLOW = 2;
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 };
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 };
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 };
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 };
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 };
}
}

859
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
{
/// <summary>
/// Strategies for deflater
/// </summary>
public enum DeflateStrategy
{
/// <summary>
/// The default strategy
/// </summary>
Default = 0,
/// <summary>
/// This strategy will only allow longer string repetitions. It is
/// useful for random data with a small character set.
/// </summary>
Filtered = 1,
/// <summary>
/// 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.
/// </summary>
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.
//
/// <summary>
/// Low level compression engine for deflate algorithm which uses a 32K sliding window
/// with secondary compression from Huffman/Shannon-Fano codes.
/// </summary>
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;
/// <summary>
/// Points to the current character in the window.
/// </summary>
private int strstart;
/// <summary>
/// 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.
/// </summary>
private int lookahead;
/// <summary>
/// The current compression function.
/// </summary>
private int compressionFunction;
/// <summary>
/// The input data for compression.
/// </summary>
private byte[] inputBuf;
/// <summary>
/// The offset into inputBuf, where input data starts.
/// </summary>
private int inputOff;
/// <summary>
/// The end offset of the input data.
/// </summary>
private int inputEnd;
private readonly DeflateStrategy strategy;
private DeflaterHuffman huffman;
private bool isDisposed;
/// <summary>
/// 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.
/// </summary>
private IMemoryOwner<short> headMemoryOwner;
private MemoryHandle headMemoryHandle;
private readonly Memory<short> head;
private readonly short* pinnedHeadPointer;
/// <summary>
/// <code>prev[index &amp; WMASK]</code> 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.
/// </summary>
private IMemoryOwner<short> prevMemoryOwner;
private MemoryHandle prevMemoryHandle;
private readonly Memory<short> prev;
private readonly short* pinnedPrevPointer;
/// <summary>
/// This array contains the part of the uncompressed stream that
/// is of relevance. The current character is indexed by strstart.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterEngine"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="strategy">The deflate strategy to use.</param>
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<short>(DeflaterConstants.HASH_SIZE);
this.head = this.headMemoryOwner.Memory;
this.headMemoryHandle = this.headMemoryOwner.Memory.Pin();
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer;
this.prevMemoryOwner = memoryAllocator.Allocate<short>(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;
}
/// <summary>
/// Gets the pending buffer to use.
/// </summary>
public DeflaterPendingBuffer Pending { get; }
/// <summary>
/// Deflate drives actual compression of data
/// </summary>
/// <param name="flush">True to flush input buffers</param>
/// <param name="finish">Finish deflation with the current input.</param>
/// <returns>Returns true if progress has been made.</returns>
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;
}
/// <summary>
/// Sets input data to be deflated. Should only be called when <see cref="NeedsInput"/>
/// returns true
/// </summary>
/// <param name="buffer">The buffer containing input data.</param>
/// <param name="offset">The offset of the first byte of data.</param>
/// <param name="count">The number of bytes of data to use as input.</param>
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;
}
/// <summary>
/// Determines if more <see cref="SetInput">input</see> is needed.
/// </summary>
/// <returns>Return true if input is needed via <see cref="SetInput">SetInput</see></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool NeedsInput() => this.inputEnd == this.inputOff;
/// <summary>
/// Reset internal state
/// </summary>
[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();
}
/// <summary>
/// Set the deflate level (0-9)
/// </summary>
/// <param name="level">The value to set the level to.</param>
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];
}
}
/// <summary>
/// Fill the window
/// </summary>
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();
}
}
/// <inheritdoc/>
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];
}
/// <summary>
/// Inserts the current string in the head hash and returns the previous
/// value for this hash.
/// </summary>
/// <returns>The previous hash value</returns>
[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);
}
}
/// <summary>
/// <para>
/// Find the best (longest) string in the window matching the
/// string starting at strstart.
/// </para>
/// <para>
/// Preconditions:
/// <code>
/// strstart + DeflaterConstants.MAX_MATCH &lt;= window.length.</code>
/// </para>
/// </summary>
/// <param name="curMatch">The current match.</param>
/// <returns>True if a match greater than the minimum length is found</returns>
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;
}
}
}

949
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
{
/// <summary>
/// Performs Deflate Huffman encoding.
/// </summary>
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<short> distanceManagedBuffer;
private readonly short* pinnedDistanceBuffer;
private MemoryHandle distanceBufferHandle;
private readonly IMemoryOwner<short> 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;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterHuffman"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
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<short>(BufferSize);
this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin();
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer;
this.literalManagedBuffer = memoryAllocator.Allocate<short>(BufferSize);
this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin();
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
/// <summary>
/// Gets the pending buffer to use.
/// </summary>
public DeflaterPendingBuffer Pending { get; private set; }
/// <summary>
/// Reset internal state
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.lastLiteral = 0;
this.extraBits = 0;
this.literalTree.Reset();
this.distTree.Reset();
this.blTree.Reset();
}
/// <summary>
/// Write all trees to pending buffer
/// </summary>
/// <param name="blTreeCodes">The number/rank of treecodes to send.</param>
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);
}
/// <summary>
/// Compress current buffer writing data to pending buffer
/// </summary>
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);
}
/// <summary>
/// Flush block to output with no compression
/// </summary>
/// <param name="stored">Data to write</param>
/// <param name="storedOffset">Index of first byte to write</param>
/// <param name="storedLength">Count of bytes to write</param>
/// <param name="lastBlock">True if this is the last block</param>
[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();
}
/// <summary>
/// Flush block to output with compression
/// </summary>
/// <param name="stored">Data to flush</param>
/// <param name="storedOffset">Index of first byte to flush</param>
/// <param name="storedLength">Count of bytes to flush</param>
/// <param name="lastBlock">True if this is the last block</param>
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<byte>(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<byte>(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();
}
}
/// <summary>
/// Get value indicating if internal buffer is full
/// </summary>
/// <returns>true if buffer is full</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsFull() => this.lastLiteral >= BufferSize;
/// <summary>
/// Add literal to buffer
/// </summary>
/// <param name="literal">Literal value to add to buffer.</param>
/// <returns>Value indicating internal buffer is full</returns>
[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();
}
/// <summary>
/// Add distance code and length to literal and distance trees
/// </summary>
/// <param name="distance">Distance code</param>
/// <param name="length">Length</param>
/// <returns>Value indicating if internal buffer is full</returns>
[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();
}
/// <summary>
/// Reverse the bits of a 16 bit value.
/// </summary>
/// <param name="toReverse">Value to reverse bits</param>
/// <returns>Value with bits reversed</returns>
[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]);
}
/// <inheritdoc/>
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<short> codesMemoryOwner;
private MemoryHandle codesMemoryHandle;
private readonly short* codes;
private IMemoryOwner<short> 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<short>(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<short>(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; }
/// <summary>
/// Resets the internal state of the tree
/// </summary>
[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]);
/// <summary>
/// Set static codes and length
/// </summary>
/// <param name="staticCodes">new codes</param>
/// <param name="staticLengths">length for new codes</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void SetStaticCodes(ReadOnlySpan<short> staticCodes, ReadOnlySpan<byte> staticLengths)
{
staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span);
staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span);
}
/// <summary>
/// Build dynamic codes and lengths
/// </summary>
public void BuildCodes()
{
// Maxes out at 15 * 4
Span<int> nextCode = stackalloc int[this.maxLength];
ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode);
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(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<int> heapMemoryOwner = this.memoryAllocator.Allocate<int>(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<int> childrenMemoryOwner = this.memoryAllocator.Allocate<int>(childrenLength))
using (IMemoryOwner<int> valuesMemoryOwner = this.memoryAllocator.Allocate<int>((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);
}
}
}
/// <summary>
/// Get encoded length
/// </summary>
/// <returns>Encoded length, the sum of frequencies * lengths</returns>
[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;
}
/// <summary>
/// Scan a literal or distance tree to determine the frequencies of the codes
/// in the bit length tree.
/// </summary>
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]++;
}
}
}
/// <summary>
/// Write the tree values.
/// </summary>
/// <param name="pendingBuffer">The pending buffer.</param>
/// <param name="bitLengthTree">The tree to write.</param>
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<int> children)
{
byte* lengthPtr = this.Length;
ref int childrenRef = ref MemoryMarshal.GetReference(children);
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(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<int> lengthsMemoryOwner = this.memoryAllocator.Allocate<int>(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);
}
}
}
}

153
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
{
/// <summary>
/// A special stream deflating or compressing the bytes that are
/// written to it. It uses a Deflater to perform actual deflating.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterOutputStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="rawStream">The output stream where deflated output is written.</param>
/// <param name="compressionLevel">The compression level.</param>
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);
}
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => this.rawStream.CanWrite;
/// <inheritdoc/>
public override long Length => this.rawStream.Length;
/// <inheritdoc/>
public override long Position
{
get
{
return this.rawStream.Position;
}
set
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte() => throw new NotSupportedException();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Flush()
{
this.deflater.Flush();
this.Deflate(true);
this.rawStream.Flush();
}
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
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);
}
}
}
}

179
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
{
/// <summary>
/// Stores pending data for writing data to the Deflater.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterPendingBuffer"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
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;
}
/// <summary>
/// Gets the number of bits written to the buffer.
/// </summary>
public int BitCount { get; private set; }
/// <summary>
/// Gets a value indicating whether indicates the buffer has been flushed.
/// </summary>
public bool IsFlushed => this.end == 0;
/// <summary>
/// Clear internal state/buffers.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset() => this.start = this.end = this.BitCount = 0;
/// <summary>
/// Write a short value to buffer LSB first.
/// </summary>
/// <param name="value">The value to write.</param>
[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));
}
/// <summary>
/// Write a block of data to the internal buffer.
/// </summary>
/// <param name="block">The data to write.</param>
/// <param name="offset">The offset of first byte to write.</param>
/// <param name="length">The number of bytes to write.</param>
[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;
}
/// <summary>
/// Aligns internal buffer on a byte boundary.
/// </summary>
[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;
}
/// <summary>
/// Write bits to internal buffer
/// </summary>
/// <param name="b">source of bits</param>
/// <param name="count">number of bits to write</param>
[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;
}
}
/// <summary>
/// Write a short value to internal buffer most significant byte first
/// </summary>
/// <param name="value">The value to write</param>
[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);
}
/// <summary>
/// Flushes the pending buffer into the given output array.
/// If the output array is to small, only a partial flush is done.
/// </summary>
/// <param name="output">The output array.</param>
/// <param name="offset">The offset into output array.</param>
/// <param name="length">The maximum number of bytes to store.</param>
/// <returns>The number of bytes flushed.</returns>
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;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.bufferMemoryHandle.Dispose();
this.bufferMemoryOwner.Dispose();
this.bufferMemoryOwner = null;
this.isDisposed = true;
}
GC.SuppressFinalize(this);
}
}
}

7
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

83
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
/// <summary>
/// The stream responsible for compressing the input stream.
/// </summary>
private System.IO.Compression.DeflateStream deflateStream;
// private DeflateStream deflateStream;
private DeflaterOutputStream deflateStream;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="stream">The stream to compress.</param>
/// <param name="compressionLevel">The compression level.</param>
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);
}
/// <inheritdoc/>
@ -110,41 +100,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => true;
public override bool CanWrite => this.rawStream.CanWrite;
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
public override long Length => this.rawStream.Length;
/// <inheritdoc/>
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
get
{
return this.rawStream.Position;
}
set
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override void Flush()
{
this.deflateStream?.Flush();
}
public override void Flush() => this.deflateStream.Flush();
/// <inheritdoc/>
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();
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
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;
}
}

8
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);
}
}
}
}
}

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// </summary>
public static readonly TheoryData<int> CompressionLevels = new TheoryData<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
};
public static readonly TheoryData<int> PaletteSizes = new TheoryData<int>

Loading…
Cancel
Save