From 7291986928981c6b5d0058ce3815ccc948324eb7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 22 Aug 2026 16:42:45 +1000 Subject: [PATCH] Switch Zlib compression to BCL stream Replace the custom zlib deflate stack with `System.IO.Compression.ZLibStream`, and add `ChunkedWriteStream` to keep PNG/TIFF/EXR output framed in fixed-size segments. Update the affected compressors and tests to match the new streaming path. --- src/ImageSharp/Compression/Zlib/Adler32.cs | 435 -------- .../Compression/Zlib/ChunkedWriteStream.cs | 143 +++ .../Compression/Zlib/DeflateThrowHelper.cs | 33 - src/ImageSharp/Compression/Zlib/Deflater.cs | 290 ------ .../Compression/Zlib/DeflaterConstants.cs | 148 --- .../Compression/Zlib/DeflaterEngine.cs | 867 ---------------- .../Compression/Zlib/DeflaterHuffman.cs | 979 ------------------ .../Compression/Zlib/DeflaterOutputStream.cs | 143 --- .../Compression/Zlib/DeflaterPendingBuffer.cs | 185 ---- src/ImageSharp/Compression/Zlib/README.md | 11 - .../Compression/Zlib/ZlibDeflateStream.cs | 177 ---- .../Compressors/ZipExrCompressor.cs | 32 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 85 +- .../Compressors/DeflateCompressor.cs | 22 +- .../General/Adler32Benchmark.cs | 70 -- .../Zlib/ChunkedWriteStreamTests.cs | 96 ++ .../Formats/Png/Adler32Tests.cs | 69 -- .../DeflateTiffCompressionTests.cs | 3 +- 18 files changed, 295 insertions(+), 3493 deletions(-) delete mode 100644 src/ImageSharp/Compression/Zlib/Adler32.cs create mode 100644 src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs delete mode 100644 src/ImageSharp/Compression/Zlib/Deflater.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflaterConstants.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflaterEngine.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs delete mode 100644 src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs delete mode 100644 src/ImageSharp/Compression/Zlib/README.md delete mode 100644 src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs delete mode 100644 tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs create mode 100644 tests/ImageSharp.Tests/Compression/Zlib/ChunkedWriteStreamTests.cs delete mode 100644 tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs diff --git a/src/ImageSharp/Compression/Zlib/Adler32.cs b/src/ImageSharp/Compression/Zlib/Adler32.cs deleted file mode 100644 index 6f2185081..000000000 --- a/src/ImageSharp/Compression/Zlib/Adler32.cs +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; - -#pragma warning disable IDE0007 // Use implicit type -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// Calculates the 32 bit Adler checksum of a given buffer according to -/// RFC 1950. ZLIB Compressed Data Format Specification version 3.3) -/// -internal static class Adler32 -{ - /// - /// The default initial seed value of a Adler32 checksum calculation. - /// - public const uint SeedValue = 1U; - - // Largest prime smaller than 65536 - private const uint BASE = 65521; - - // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 - private const uint NMAX = 5552; - - private const int MinBufferSize = 64; - - private const int BlockSize = 1 << 5; - - // The C# compiler emits this as a compile-time constant embedded in the PE file. - private static ReadOnlySpan Tap1Tap2 => - [ - 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1 - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2 - ]; - - /// - /// Calculates the Adler32 checksum with the bytes taken from the span. - /// - /// The readonly span of bytes. - /// The . - [MethodImpl(InliningOptions.ShortMethod)] - public static uint Calculate(ReadOnlySpan buffer) - => Calculate(SeedValue, buffer); - - /// - /// Calculates the Adler32 checksum with the bytes taken from the span and seed. - /// - /// The input Adler32 value. - /// The readonly span of bytes. - /// The . - [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] - public static uint Calculate(uint adler, ReadOnlySpan buffer) - { - if (buffer.IsEmpty) - { - return adler; - } - - if (Avx2.IsSupported && buffer.Length >= MinBufferSize) - { - return CalculateAvx2(adler, buffer); - } - - if (Ssse3.IsSupported && buffer.Length >= MinBufferSize) - { - return CalculateSse(adler, buffer); - } - - if (AdvSimd.IsSupported) - { - return CalculateArm(adler, buffer); - } - - return CalculateScalar(adler, buffer); - } - - // Based on https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c - [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] - private static unsafe uint CalculateSse(uint adler, ReadOnlySpan buffer) - { - uint s1 = adler & 0xFFFF; - uint s2 = (adler >> 16) & 0xFFFF; - - // Process the data in blocks. - uint length = (uint)buffer.Length; - uint blocks = length / BlockSize; - length -= blocks * BlockSize; - - fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) - { - fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2)) - { - byte* localBufferPtr = bufferPtr; - - // _mm_setr_epi8 on x86 - Vector128 tap1 = Sse2.LoadVector128((sbyte*)tapPtr); - Vector128 tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10)); - Vector128 zero = Vector128.Zero; - Vector128 ones = Vector128.Create((short)1); - - while (blocks > 0) - { - uint n = NMAX / BlockSize; /* The NMAX constraint. */ - if (n > blocks) - { - n = blocks; - } - - blocks -= n; - - // Process n blocks of data. At most NMAX data bytes can be - // processed before s2 must be reduced modulo BASE. - Vector128 v_ps = Vector128.CreateScalar(s1 * n); - Vector128 v_s2 = Vector128.CreateScalar(s2); - Vector128 v_s1 = Vector128.Zero; - - do - { - // Load 32 input bytes. - Vector128 bytes1 = Sse3.LoadDquVector128(localBufferPtr); - Vector128 bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10); - - // Add previous block byte sum to v_ps. - v_ps = Sse2.Add(v_ps, v_s1); - - // Horizontally add the bytes for s1, multiply-adds the - // bytes by [ 32, 31, 30, ... ] for s2. - v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsUInt32()); - Vector128 mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1); - v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones).AsUInt32()); - - v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsUInt32()); - Vector128 mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2); - v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32()); - - localBufferPtr += BlockSize; - } - while (--n > 0); - - v_s2 = Sse2.Add(v_s2, Sse2.ShiftLeftLogical(v_ps, 5)); - - // Sum epi32 ints v_s1(s2) and accumulate in s1(s2). - const byte s2301 = 0b1011_0001; // A B C D -> B A D C - const byte s1032 = 0b0100_1110; // A B C D -> C D A B - - v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, s1032)); - - s1 += v_s1.ToScalar(); - - v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s2301)); - v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s1032)); - - s2 = v_s2.ToScalar(); - - // Reduce. - s1 %= BASE; - s2 %= BASE; - } - - if (length > 0) - { - HandleLeftOver(localBufferPtr, length, ref s1, ref s2); - } - - return s1 | (s2 << 16); - } - } - } - - // Based on: https://github.com/zlib-ng/zlib-ng/blob/develop/arch/x86/adler32_avx2.c - [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] - public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan buffer) - { - uint s1 = adler & 0xFFFF; - uint s2 = (adler >> 16) & 0xFFFF; - uint length = (uint)buffer.Length; - - fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) - { - byte* localBufferPtr = bufferPtr; - - Vector256 zero = Vector256.Zero; - Vector256 dot3v = Vector256.Create((short)1); - Vector256 dot2v = Vector256.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); - - // Process n blocks of data. At most NMAX data bytes can be - // processed before s2 must be reduced modulo BASE. - Vector256 vs1 = Vector256.CreateScalar(s1); - Vector256 vs2 = Vector256.CreateScalar(s2); - - while (length >= 32) - { - int k = length < NMAX ? (int)length : (int)NMAX; - k -= k % 32; - length -= (uint)k; - - Vector256 vs10 = vs1; - Vector256 vs3 = Vector256.Zero; - - while (k >= 32) - { - // Load 32 input bytes. - Vector256 block = Avx.LoadVector256(localBufferPtr); - - // Sum of abs diff, resulting in 2 x int32's - Vector256 vs1sad = Avx2.SumAbsoluteDifferences(block, zero); - - vs1 = Avx2.Add(vs1, vs1sad.AsUInt32()); - vs3 = Avx2.Add(vs3, vs10); - - // sum 32 uint8s to 16 shorts. - Vector256 vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v); - - // sum 16 shorts to 8 uint32s. - Vector256 vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v); - - vs2 = Avx2.Add(vsum2.AsUInt32(), vs2); - vs10 = vs1; - - localBufferPtr += BlockSize; - k -= 32; - } - - // Defer the multiplication with 32 to outside of the loop. - vs3 = Avx2.ShiftLeftLogical(vs3, 5); - vs2 = Avx2.Add(vs2, vs3); - - s1 = (uint)Numerics.EvenReduceSum(vs1.AsInt32()); - s2 = (uint)Numerics.ReduceSum(vs2.AsInt32()); - - s1 %= BASE; - s2 %= BASE; - - vs1 = Vector256.CreateScalar(s1); - vs2 = Vector256.CreateScalar(s2); - } - - if (length > 0) - { - HandleLeftOver(localBufferPtr, length, ref s1, ref s2); - } - - return s1 | (s2 << 16); - } - } - - // Based on: https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c - [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] - private static unsafe uint CalculateArm(uint adler, ReadOnlySpan buffer) - { - // Split Adler-32 into component sums. - uint s1 = adler & 0xFFFF; - uint s2 = (adler >> 16) & 0xFFFF; - uint length = (uint)buffer.Length; - - // Process the data in blocks. - long blocks = length / BlockSize; - length -= (uint)(blocks * BlockSize); - fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) - { - byte* localBufferPtr = bufferPtr; - - while (blocks != 0) - { - uint n = NMAX / BlockSize; - if (n > blocks) - { - n = (uint)blocks; - } - - blocks -= n; - - // Process n blocks of data. At most nMax data bytes can be - // processed before s2 must be reduced modulo Base. - Vector128 vs1 = Vector128.Zero; - Vector128 vs2 = vs1.WithElement(3, s1 * n); - Vector128 vColumnSum1 = Vector128.Zero; - Vector128 vColumnSum2 = Vector128.Zero; - Vector128 vColumnSum3 = Vector128.Zero; - Vector128 vColumnSum4 = Vector128.Zero; - - do - { - // Load 32 input bytes. - Vector128 bytes1 = AdvSimd.LoadVector128(localBufferPtr).AsUInt16(); - Vector128 bytes2 = AdvSimd.LoadVector128(localBufferPtr + 0x10).AsUInt16(); - - // Add previous block byte sum to v_s2. - vs2 = AdvSimd.Add(vs2, vs1); - - // Horizontally add the bytes for s1. - vs1 = AdvSimd.AddPairwiseWideningAndAdd( - vs1.AsUInt32(), - AdvSimd.AddPairwiseWideningAndAdd(AdvSimd.AddPairwiseWidening(bytes1.AsByte()).AsUInt16(), bytes2.AsByte())); - - // Vertically add the bytes for s2. - vColumnSum1 = AdvSimd.AddWideningLower(vColumnSum1, bytes1.GetLower().AsByte()); - vColumnSum2 = AdvSimd.AddWideningLower(vColumnSum2, bytes1.GetUpper().AsByte()); - vColumnSum3 = AdvSimd.AddWideningLower(vColumnSum3, bytes2.GetLower().AsByte()); - vColumnSum4 = AdvSimd.AddWideningLower(vColumnSum4, bytes2.GetUpper().AsByte()); - - localBufferPtr += BlockSize; - } - while (--n > 0); - - vs2 = AdvSimd.ShiftLeftLogical(vs2, 5); - - // Multiply-add bytes by [ 32, 31, 30, ... ] for s2. - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetLower(), Vector64.Create((ushort)32, 31, 30, 29)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetUpper(), Vector64.Create((ushort)28, 27, 26, 25)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetLower(), Vector64.Create((ushort)24, 23, 22, 21)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetUpper(), Vector64.Create((ushort)20, 19, 18, 17)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetLower(), Vector64.Create((ushort)16, 15, 14, 13)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetUpper(), Vector64.Create((ushort)12, 11, 10, 9)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetLower(), Vector64.Create((ushort)8, 7, 6, 5)); - vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetUpper(), Vector64.Create((ushort)4, 3, 2, 1)); - - // Sum epi32 ints v_s1(s2) and accumulate in s1(s2). - Vector64 sum1 = AdvSimd.AddPairwise(vs1.GetLower(), vs1.GetUpper()); - Vector64 sum2 = AdvSimd.AddPairwise(vs2.GetLower(), vs2.GetUpper()); - Vector64 s1s2 = AdvSimd.AddPairwise(sum1, sum2); - - // Store the results. - s1 += AdvSimd.Extract(s1s2, 0); - s2 += AdvSimd.Extract(s1s2, 1); - - // Reduce. - s1 %= BASE; - s2 %= BASE; - } - - if (length > 0) - { - HandleLeftOver(localBufferPtr, length, ref s1, ref s2); - } - - return s1 | (s2 << 16); - } - } - - private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2) - { - if (length >= 16) - { - s2 += s1 += localBufferPtr[0]; - s2 += s1 += localBufferPtr[1]; - s2 += s1 += localBufferPtr[2]; - s2 += s1 += localBufferPtr[3]; - s2 += s1 += localBufferPtr[4]; - s2 += s1 += localBufferPtr[5]; - s2 += s1 += localBufferPtr[6]; - s2 += s1 += localBufferPtr[7]; - s2 += s1 += localBufferPtr[8]; - s2 += s1 += localBufferPtr[9]; - s2 += s1 += localBufferPtr[10]; - s2 += s1 += localBufferPtr[11]; - s2 += s1 += localBufferPtr[12]; - s2 += s1 += localBufferPtr[13]; - s2 += s1 += localBufferPtr[14]; - s2 += s1 += localBufferPtr[15]; - - localBufferPtr += 16; - length -= 16; - } - - while (length-- > 0) - { - s2 += s1 += *localBufferPtr++; - } - - if (s1 >= BASE) - { - s1 -= BASE; - } - - s2 %= BASE; - } - - [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] - private static unsafe uint CalculateScalar(uint adler, ReadOnlySpan buffer) - { - uint s1 = adler & 0xFFFF; - uint s2 = (adler >> 16) & 0xFFFF; - - fixed (byte* bufferPtr = buffer) - { - byte* localBufferPtr = bufferPtr; - uint length = (uint)buffer.Length; - - while (length > 0) - { - uint k = length < NMAX ? length : NMAX; - length -= k; - - while (k >= 16) - { - s2 += s1 += localBufferPtr[0]; - s2 += s1 += localBufferPtr[1]; - s2 += s1 += localBufferPtr[2]; - s2 += s1 += localBufferPtr[3]; - s2 += s1 += localBufferPtr[4]; - s2 += s1 += localBufferPtr[5]; - s2 += s1 += localBufferPtr[6]; - s2 += s1 += localBufferPtr[7]; - s2 += s1 += localBufferPtr[8]; - s2 += s1 += localBufferPtr[9]; - s2 += s1 += localBufferPtr[10]; - s2 += s1 += localBufferPtr[11]; - s2 += s1 += localBufferPtr[12]; - s2 += s1 += localBufferPtr[13]; - s2 += s1 += localBufferPtr[14]; - s2 += s1 += localBufferPtr[15]; - - localBufferPtr += 16; - k -= 16; - } - - while (k-- > 0) - { - s2 += s1 += *localBufferPtr++; - } - - s1 %= BASE; - s2 %= BASE; - } - - return (s2 << 16) | s1; - } - } -} diff --git a/src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs b/src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs new file mode 100644 index 000000000..44f655011 --- /dev/null +++ b/src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Compression.Zlib; + +/// +/// A write-only stream that groups written bytes into fixed-length segments. Bytes are +/// collected in a pooled segment buffer; when the buffer is full the supplied delegate is +/// invoked with the completed segment and the buffer is reused. The final partial segment, +/// if any, is emitted on disposal. The delegate owns the destination; this stream writes +/// nowhere itself and is the write-side counterpart of . +/// +internal sealed class ChunkedWriteStream : Stream +{ + /// + /// The segment length used when the caller does not require a specific framing size. + /// + public const int DefaultSegmentLength = 64 * 1024; + + private readonly IMemoryOwner segmentOwner; + private readonly Memory segment; + private readonly Action> writeSegment; + private int segmentFilled; + private bool isDisposed; + + /// + /// Initializes a new instance of the class using . + /// + /// The memory allocator used to rent the segment buffer. + /// Invoked with each completed segment, and with the final partial segment on disposal. + public ChunkedWriteStream(MemoryAllocator allocator, Action> writeSegment) + : this(allocator, DefaultSegmentLength, writeSegment) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator used to rent the segment buffer. + /// The length of each completed segment. + /// Invoked with each completed segment, and with the final partial segment on disposal. + public ChunkedWriteStream(MemoryAllocator allocator, int segmentLength, Action> writeSegment) + { + this.segmentOwner = allocator.Allocate(segmentLength); + this.segment = this.segmentOwner.Memory; + this.writeSegment = writeSegment; + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + /// + /// Does nothing. A segment is emitted only when it is full or on disposal, so the segment + /// length stays fixed however often the producer flushes. + /// + public override void Flush() + { + } + + /// + 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 void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void WriteByte(byte value) + { + this.segment.Span[this.segmentFilled++] = value; + this.EmitIfFull(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) => this.Write(buffer.AsSpan(offset, count)); + + /// + public override void Write(ReadOnlySpan buffer) + { + Span segment = this.segment.Span; + while (!buffer.IsEmpty) + { + int count = Math.Min(segment.Length - this.segmentFilled, buffer.Length); + buffer[..count].CopyTo(segment[this.segmentFilled..]); + this.segmentFilled += count; + buffer = buffer[count..]; + this.EmitIfFull(); + } + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + if (disposing) + { + // The producer has finished, so the partial segment is the final one. + if (this.segmentFilled > 0) + { + this.writeSegment(this.segment.Span[..this.segmentFilled]); + this.segmentFilled = 0; + } + + this.segmentOwner.Dispose(); + } + + base.Dispose(disposing); + } + + /// + /// Emits the segment buffer when it is full and resets it for reuse. + /// + private void EmitIfFull() + { + if (this.segmentFilled == this.segment.Length) + { + this.writeSegment(this.segment.Span); + this.segmentFilled = 0; + } + } +} diff --git a/src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs deleted file mode 100644 index 30761328f..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -internal static class DeflateThrowHelper -{ - [DoesNotReturn] - public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); - - [DoesNotReturn] - public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); - - [DoesNotReturn] - public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); - - [DoesNotReturn] - public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); - - [DoesNotReturn] - public static void ThrowNull(string name) => throw new ArgumentNullException(name); - - [DoesNotReturn] - public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); - - [DoesNotReturn] - public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); - - [DoesNotReturn] - public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); -} diff --git a/src/ImageSharp/Compression/Zlib/Deflater.cs b/src/ImageSharp/Compression/Zlib/Deflater.cs deleted file mode 100644 index f642ec85a..000000000 --- a/src/ImageSharp/Compression/Zlib/Deflater.cs +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.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(Span 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.isDisposed = true; - } - } -} diff --git a/src/ImageSharp/Compression/Zlib/DeflaterConstants.cs b/src/ImageSharp/Compression/Zlib/DeflaterConstants.cs deleted file mode 100644 index fbc2083b3..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflaterConstants.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -// -using System; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// This class contains constants used for deflation. -/// -internal 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/Compression/Zlib/DeflaterEngine.cs b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs deleted file mode 100644 index 6009fdfbc..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs +++ /dev/null @@ -1,867 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// Strategies for deflater -/// -internal 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 IMemoryOwner windowMemoryOwner; - private MemoryHandle windowMemoryHandle; - private readonly Memory 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.Allocate(2 * DeflaterConstants.WSIZE); - this.window = this.windowMemoryOwner.Memory; - this.windowMemoryHandle = this.window.Pin(); - this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; - - this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE); - this.head = this.headMemoryOwner.Memory; - this.headMemoryHandle = this.head.Pin(); - this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; - - this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE); - this.prev = this.prevMemoryOwner.Memory; - this.prevMemoryHandle = this.prev.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[..DeflaterConstants.HASH_SIZE].Clear(); - this.prev.Span[..DeflaterConstants.WSIZE].Clear(); - } - - /// - /// Set the deflate level (0-9) - /// - /// The value to set the level to. - public void SetLevel(int level) - { - if (level is < 0 or > 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.Span, 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.Span, 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.Span, 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; - } - - ArgumentNullException.ThrowIfNull(this.inputBuf); - - Unsafe.CopyBlockUnaligned( - ref this.window.Span[this.strstart + this.lookahead], - ref this.inputBuf[this.inputOff], - unchecked((uint)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.isDisposed = true; - } - } - - [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 MemoryMarshal.GetReference(this.window.Span), - ref Unsafe.Add(ref MemoryMarshal.GetReference(this.window.Span), 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 - [MethodImpl(InliningOptions.HotPath)] - 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; - int matchLength = this.matchLen; - matchLength = Math.Max(matchLength, DeflaterConstants.MIN_MATCH - 1); - this.matchLen = matchLength; - - if (scan > scanMax - matchLength) - { - return false; - } - - int scanEndPosition = scan + matchLength; - - byte* pinnedWindow = this.pinnedWindowPointer; - int scanStart = this.strstart; - byte scanEnd1 = pinnedWindow[scanEndPosition - 1]; - byte scanEnd = pinnedWindow[scanEndPosition]; - - // 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; - - int matchEndPosition = match + matchLength; - if (pinnedWindow[matchEndPosition] != scanEnd - || pinnedWindow[matchEndPosition - 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.Span, 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; - } - - const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; - while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) - { - if (this.lookahead == 0) - { - // We are flushing everything - this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish); - this.blockStart = this.strstart; - return false; - } - - if (this.strstart > windowLen) - { - // 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.Span, 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; - } - - const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; - 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.Span, this.blockStart, this.strstart - this.blockStart, finish); - this.blockStart = this.strstart; - return false; - } - - if (this.strstart >= windowLen) - { - // 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.Span, this.blockStart, len, lastBlock); - this.blockStart += len; - return !lastBlock; - } - } - - return true; - } -} diff --git a/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs deleted file mode 100644 index 17cb1925d..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs +++ /dev/null @@ -1,979 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.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; - - private Tree literalTree; - private Tree distTree; - private Tree blTree; - - // Buffer for distances - private readonly IMemoryOwner distanceMemoryOwner; - private readonly short* pinnedDistanceBuffer; - private MemoryHandle distanceBufferHandle; - - private readonly IMemoryOwner literalMemoryOwner; - private readonly short* pinnedLiteralBuffer; - private MemoryHandle literalBufferHandle; - - private int lastLiteral; - private int extraBits; - private bool isDisposed; - - /// - /// 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.distanceMemoryOwner = memoryAllocator.Allocate(BufferSize); - this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin(); - this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; - - this.literalMemoryOwner = memoryAllocator.Allocate(BufferSize); - this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin(); - this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; - } - -#pragma warning disable SA1201 // Elements should appear in the correct order - - // See RFC 1951 3.2.6 - // Literal codes - private static readonly short[] StaticLCodes = - [ - 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, - 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, - 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, - 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, - 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, - 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, - 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, - 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, - 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19, - 275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499, - 11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491, - 27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507, - 7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487, - 23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503, - 15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495, - 31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511, - 0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36, - 100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163 - ]; - - private static ReadOnlySpan StaticLLength => - [ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8 - ]; - - // Distance codes and lengths. - private static readonly short[] StaticDCodes = - [ - 0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, - 30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23 - ]; - - private static ReadOnlySpan StaticDLength => - [ - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 - ]; -#pragma warning restore SA1201 // Elements should appear in the correct order - - /// - /// Gets 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 ReadOnlySpan BitLengthOrder => - [ - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 - ]; - - private static ReadOnlySpan Bit4Reverse => - [ - 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 - ]; - - /// - /// 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 = (int)(((uint)lc - 261) / 4); - if (bits is > 0 and <= 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(ReadOnlySpan 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(ReadOnlySpan 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 (nuint i = 0; i < LiteralNumber; i++) - { - static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); - } - - ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); - for (nuint 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 += (int)(((uint)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) - { - /* Use unsafe offsetting and manually validate the input index to reduce the - * total number of conditional branches. There are two main cases to test here: - * 1. In the first 3, the input value (or some combination of it) is combined - * with & 0xF, which results in a maximum value of 0xF no matter what the - * input value was. That is 15, which is always in range for the target span. - * As a result, no input validation is needed at all in this case. - * 2. There are two cases where the input value might cause an invalid access: - * when it is either negative, or greater than 15 << 12. We can test both - * conditions in a single pass by casting the input value to uint and right - * shifting it by 12, which also preserves the sign. If it is a negative - * value (2-complement), the test will fail as the uint cast will result - * in a much larger value. If the value was simply too high, the test will - * fail as expected. We can't simply check whether the value is lower than - * 15 << 12, because higher values are acceptable in the first 3 accesses. - * Doing this reduces the total number of index checks from 4 down to just 1. */ - int toReverseRightShiftBy12 = toReverse >> 12; - Guard.MustBeLessThanOrEqualTo((uint)toReverseRightShiftBy12, 15, nameof(toReverse)); - - ref byte bit4ReverseRef = ref MemoryMarshal.GetReference(Bit4Reverse); - - return (short)((Unsafe.Add(ref bit4ReverseRef, (uint)toReverse & 0xF) << 12) - | (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 4) & 0xF) << 8) - | (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 8) & 0xF) << 4) - | Unsafe.Add(ref bit4ReverseRef, (uint)toReverseRightShiftBy12)); - } - - /// - public void Dispose() - { - if (!this.isDisposed) - { - this.Pending.Dispose(); - this.distanceBufferHandle.Dispose(); - this.distanceMemoryOwner.Dispose(); - this.literalBufferHandle.Dispose(); - this.literalMemoryOwner.Dispose(); - - this.literalTree.Dispose(); - this.blTree.Dispose(); - this.distTree.Dispose(); - - this.isDisposed = true; - } - } - - [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 IMemoryOwner 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.Allocate(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, (uint)bits) = code; - code += Unsafe.Add(ref bitLengthCountsRef, (uint)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, (uint)(bits - 1))); - Unsafe.Add(ref nextCodeRef, (uint)(bits - 1)) += 1 << (16 - bits); - } - } - } - - [MethodImpl(InliningOptions.HotPath)] - 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, (uint)(ppos = (pos - 1) >> 1))] > freq) - { - Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, (uint)ppos); - pos = ppos; - } - - Unsafe.Add(ref heapRef, (uint)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, (uint)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 (nuint i = 0; i < (uint)heapLen; i++) - { - int node = Unsafe.Add(ref heapRef, i); - nuint 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) = (int)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, (uint)--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, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) - { - path++; - } - - Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)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, (uint)last); - while ((path = ppos) > 0 - && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) - { - Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); - } - - Unsafe.Add(ref heapRef, (uint)path) = last; - - int second = Unsafe.Add(ref heapRef, 0); - - // Create a new node father of first and second - last = numNodes++; - Unsafe.Add(ref childrenRef, (uint)(2 * last)) = first; - Unsafe.Add(ref childrenRef, (uint)((2 * last) + 1)) = second; - int mindepth = Math.Min(Unsafe.Add(ref valuesRef, (uint)first) & 0xFF, Unsafe.Add(ref valuesRef, (uint)second) & 0xFF); - Unsafe.Add(ref valuesRef, (uint)last) = lastVal = Unsafe.Add(ref valuesRef, (uint)first) + Unsafe.Add(ref valuesRef, (uint)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, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) - { - path++; - } - - Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path); - ppos = path; - path = (ppos * 2) + 1; - } - - // Now propagate the new element down along path - while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) - { - Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); - } - - Unsafe.Add(ref heapRef, (uint)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, (uint)i) + 1; - if (bitLength > maxLen) - { - bitLength = maxLen; - overflow++; - } - - Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)(2 * i))) = Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)((2 * i) + 1))) = bitLength; - } - else - { - // A leaf node - int bitLength = Unsafe.Add(ref lengthsRef, (uint)i); - Unsafe.Add(ref bitLengthCountsRef, (uint)(bitLength - 1))++; - lengthPtr[Unsafe.Add(ref childrenRef, (uint)(2 * i))] = (byte)Unsafe.Add(ref lengthsRef, (uint)i); - } - } - } - - if (overflow == 0) - { - return; - } - - int incrBitLen = maxLen - 1; - do - { - // Find the first bit length which could increase: - while (Unsafe.Add(ref bitLengthCountsRef, (uint)--incrBitLen) == 0) - { - } - - // Move this node one down and remove a corresponding - // number of overflow nodes. - do - { - Unsafe.Add(ref bitLengthCountsRef, (uint)incrBitLen)--; - Unsafe.Add(ref bitLengthCountsRef, (uint)++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, (uint)(maxLen - 1)) += overflow; - Unsafe.Add(ref bitLengthCountsRef, (uint)(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, (uint)(bits - 1)); - while (n > 0) - { - int childIndex = 2 * Unsafe.Add(ref childrenRef, (uint)nodeIndex++); - if (Unsafe.Add(ref childrenRef, (uint)(childIndex + 1)) == -1) - { - // We found another leaf - lengthPtr[Unsafe.Add(ref childrenRef, (uint)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.isDisposed = true; - } - } - } -} diff --git a/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs deleted file mode 100644 index de818fd8f..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.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 IMemoryOwner memoryOwner; - private readonly Memory 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.Allocate(BufferLength); - this.buffer = this.memoryOwner.Memory; - 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 => 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.Span, 0, BufferLength); - - if (deflateCount <= 0) - { - break; - } - - this.rawStream.Write(this.buffer.Span[..deflateCount]); - } - - if (!this.deflater.IsNeedingInput) - { - DeflateThrowHelper.ThrowNoDeflate(); - } - } - - private void Finish() - { - this.deflater.Finish(); - while (!this.deflater.IsFinished) - { - int len = this.deflater.Deflate(this.buffer.Span, 0, BufferLength); - if (len <= 0) - { - break; - } - - this.rawStream.Write(this.buffer.Span[..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.isDisposed = true; - base.Dispose(disposing); - } - } -} diff --git a/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs deleted file mode 100644 index 37e7404e4..000000000 --- a/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// Stores pending data for writing data to the Deflater. -/// -internal sealed unsafe class DeflaterPendingBuffer : IDisposable -{ - private readonly Memory buffer; - private readonly byte* pinnedBuffer; - private IMemoryOwner 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.Allocate(DeflaterConstants.PENDING_BUF_SIZE); - this.buffer = this.bufferMemoryOwner.Memory; - this.bufferMemoryHandle = this.buffer.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(ReadOnlySpan block, int offset, int length) - { - Unsafe.CopyBlockUnaligned( - ref this.buffer.Span[this.end], - ref MemoryMarshal.GetReference(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(Span 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.Span[this.start], - unchecked((uint)length)); - this.start = 0; - this.end = 0; - } - else - { - Unsafe.CopyBlockUnaligned( - ref output[offset], - ref this.buffer.Span[this.start], - unchecked((uint)length)); - this.start += length; - } - - return length; - } - - /// - public void Dispose() - { - if (!this.isDisposed) - { - this.bufferMemoryHandle.Dispose(); - this.bufferMemoryOwner.Dispose(); - this.isDisposed = true; - } - } -} diff --git a/src/ImageSharp/Compression/Zlib/README.md b/src/ImageSharp/Compression/Zlib/README.md deleted file mode 100644 index 3875f9884..000000000 --- a/src/ImageSharp/Compression/Zlib/README.md +++ /dev/null @@ -1,11 +0,0 @@ -DeflateStream implementation adapted from - -https://github.com/icsharpcode/SharpZipLib - -Licensed under MIT - -Crc32 and Adler32 SIMD implementation adapted from - -https://github.com/chromium/chromium - -Licensed under BSD 3-Clause "New" or "Revised" License diff --git a/src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs deleted file mode 100644 index 2e52f84d7..000000000 --- a/src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm. -/// -internal sealed class ZlibDeflateStream : Stream -{ - /// - /// The raw stream containing the uncompressed image data. - /// - private readonly Stream rawStream; - - /// - /// Computes the checksum for the data stream. - /// - private uint adler = Adler32.SeedValue; - - /// - /// A value indicating whether this instance of the given entity has been disposed. - /// - /// if this instance has been disposed; otherwise, . - /// - /// If the entity is disposed, it must not be disposed a second - /// time. The isDisposed field is set the first time the entity - /// is disposed. If the isDisposed field is true, then the Dispose() - /// method will not dispose again. This help not to prolong the entity's - /// life in the Garbage Collector. - /// - private bool isDisposed; - - /// - /// The stream responsible for compressing the input stream. - /// - 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(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level) - : this(memoryAllocator, stream, (PngCompressionLevel)level) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator to use for buffer allocations. - /// The stream to compress. - /// The compression level. - public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, PngCompressionLevel level) - { - int compressionLevel = (int)level; - this.rawStream = stream; - - // Write the zlib header : http://tools.ietf.org/html/rfc1950 - // CMF(Compression Method and flags) - // This byte is divided into a 4 - bit compression method and a - // 4-bit information field depending on the compression method. - // bits 0 to 3 CM Compression method - // bits 4 to 7 CINFO Compression info - // - // 0 1 - // +---+---+ - // |CMF|FLG| - // +---+---+ - const int Cmf = 0x78; - int flg = 218; - - // http://stackoverflow.com/a/2331025/277304 - if (compressionLevel >= 5 && compressionLevel <= 6) - { - flg = 156; - } - else if (compressionLevel >= 3 && compressionLevel <= 4) - { - flg = 94; - } - else if (compressionLevel <= 2) - { - flg = 1; - } - - // Just in case - flg -= ((Cmf * 256) + flg) % 31; - - if (flg < 0) - { - flg += 31; - } - - this.rawStream.WriteByte(Cmf); - this.rawStream.WriteByte((byte)flg); - - this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, 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 void Flush() => this.deflateStream.Flush(); - - /// - 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 void SetLength(long value) => throw new NotSupportedException(); - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public override void Write(byte[] buffer, int offset, int count) - { - this.deflateStream.Write(buffer, offset, count); - this.adler = Adler32.Calculate(this.adler, buffer.AsSpan(offset, count)); - } - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - if (disposing) - { - // dispose managed resources - this.deflateStream.Dispose(); - - // Add the crc - uint crc = this.adler; - this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF)); - this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF)); - this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF)); - this.rawStream.WriteByte((byte)(crc & 0xFF)); - } - - base.Dispose(disposing); - this.isDisposed = true; - } -} diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 01248cc39..f9b68e5b8 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Memory; @@ -13,8 +14,6 @@ internal class ZipExrCompressor : ExrBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; - private readonly MemoryStream memoryStream; - private readonly System.Buffers.IMemoryOwner buffer; /// @@ -32,7 +31,6 @@ internal class ZipExrCompressor : ExrBaseCompressor { this.compressionLevel = compressionLevel; this.buffer = allocator.Allocate((int)bytesPerBlock); - this.memoryStream = new(); } /// @@ -59,28 +57,22 @@ internal class ZipExrCompressor : ExrBaseCompressor predicted[i] = (byte)d; } - this.memoryStream.Seek(0, SeekOrigin.Begin); - using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + // Compressed bytes stream straight to the output in fixed segments. The block size is + // totaled in the callback because the final partial segment is only emitted on disposal. + uint size = 0; + using (ChunkedWriteStream segmentStream = new(this.Allocator, segment => + { + this.Output.Write(segment); + size += (uint)segment.Length; + })) + using (ZLibStream stream = new(segmentStream, new ZLibCompressionOptions { CompressionLevel = (int)this.compressionLevel }, true)) { stream.Write(predicted); - stream.Flush(); } - int size = (int)this.memoryStream.Position; - byte[] buffer = this.memoryStream.GetBuffer(); - this.Output.Write(buffer, 0, size); - - // Reset memory stream for next pixel row. - this.memoryStream.Seek(0, SeekOrigin.Begin); - this.memoryStream.SetLength(0); - - return (uint)size; + return size; } /// - protected override void Dispose(bool disposing) - { - this.buffer.Dispose(); - this.memoryStream?.Dispose(); - } + protected override void Dispose(bool disposing) => this.buffer.Dispose(); } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1f5bbc41c..caf2393d8 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; using System.IO.Hashing; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -1197,7 +1198,7 @@ internal sealed class PngEncoderCore : IDisposable private byte[] GetZlibCompressedBytes(byte[] dataBytes) { using MemoryStream memoryStream = new(); - using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) + using (ZLibStream deflateStream = new(memoryStream, new ZLibCompressionOptions { CompressionLevel = (int)this.encoder.CompressionLevel }, true)) { deflateStream.Write(dataBytes); } @@ -1320,34 +1321,6 @@ internal sealed class PngEncoderCore : IDisposable private uint WriteDataChunks(in FrameControl frameControl, in Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { - byte[] buffer; - int bufferLength; - - using (MemoryStream memoryStream = new()) - { - using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) - { - if (this.interlaceMode is PngInterlaceMode.Adam7) - { - if (quantized is not null) - { - this.EncodeAdam7IndexedPixels(quantized, deflateStream); - } - else - { - this.EncodeAdam7Pixels(in frame, deflateStream); - } - } - else - { - this.EncodePixels(in frame, quantized, deflateStream); - } - } - - buffer = memoryStream.ToArray(); - bufferLength = buffer.Length; - } - // Store the chunks in repeated 64k blocks. // This reduces the memory load for decoding the image for many decoders. int maxBlockSize = MaxBlockSize; @@ -1356,36 +1329,46 @@ internal sealed class PngEncoderCore : IDisposable maxBlockSize -= 4; } - int numChunks = bufferLength / maxBlockSize; - - if (bufferLength % maxBlockSize != 0) - { - numChunks++; - } - - for (int i = 0; i < numChunks; i++) + // Compressed bytes stream straight into data chunks as each block fills, so nothing + // larger than one block is buffered. The final partial block is emitted when the + // segment stream is disposed, after the deflate stream has written its trailer. + // '1' is added to the sequence number to account for the preceding frame control chunk; + // it then increments for each frame data chunk. + uint numChunks = 0; + uint sequenceNumber = frameControl.SequenceNumber + 1; + using (ChunkedWriteStream segmentStream = new(this.memoryAllocator, maxBlockSize, segment => { - int length = bufferLength - (i * maxBlockSize); - - if (length > maxBlockSize) + if (isFrame) + { + this.WriteFrameDataChunk(stream, sequenceNumber++, segment, 0, segment.Length); + } + else { - length = maxBlockSize; + this.WriteChunk(stream, PngChunkType.Data, segment); } - if (isFrame) + numChunks++; + })) + using (ZLibStream deflateStream = new(segmentStream, new ZLibCompressionOptions { CompressionLevel = (int)this.encoder.CompressionLevel }, true)) + { + if (this.interlaceMode is PngInterlaceMode.Adam7) { - // We increment the sequence number for each frame chunk. - // '1' is added to the sequence number to account for the preceding frame control chunk. - uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i); - this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length); + if (quantized is not null) + { + this.EncodeAdam7IndexedPixels(quantized, deflateStream); + } + else + { + this.EncodeAdam7Pixels(in frame, deflateStream); + } } else { - this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length); + this.EncodePixels(in frame, quantized, deflateStream); } } - return (uint)numChunks; + return numChunks; } /// @@ -1408,7 +1391,7 @@ internal sealed class PngEncoderCore : IDisposable /// The image frame pixel buffer. /// The quantized pixels. /// The deflate stream. - private void EncodePixels(in Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(in Buffer2DRegion pixels, IndexedImageFrame? quantized, ZLibStream deflateStream) where TPixel : unmanaged, IPixel { int bytesPerScanline = this.CalculateScanlineLength(pixels.Width); @@ -1435,7 +1418,7 @@ internal sealed class PngEncoderCore : IDisposable /// The type of the pixel. /// The image frame pixel buffer. /// The deflate stream. - private void EncodeAdam7Pixels(in Buffer2DRegion pixels, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(in Buffer2DRegion pixels, ZLibStream deflateStream) where TPixel : unmanaged, IPixel { for (int pass = 0; pass < 7; pass++) @@ -1486,7 +1469,7 @@ internal sealed class PngEncoderCore : IDisposable /// The type of the pixel. /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZLibStream deflateStream) where TPixel : unmanaged, IPixel { for (int pass = 0; pass < 7; pass++) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs index 3debd373c..1f2ec2fe9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Memory; @@ -11,8 +12,6 @@ internal sealed class DeflateCompressor : TiffBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; - private readonly MemoryStream memoryStream = new(); - public DeflateCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor, DeflateCompressionLevel compressionLevel) : base(output, allocator, width, bitsPerPixel, predictor) => this.compressionLevel = compressionLevel; @@ -28,21 +27,16 @@ internal sealed class DeflateCompressor : TiffBaseCompressor /// public override void CompressStrip(Span rows, int height) { - this.memoryStream.Seek(0, SeekOrigin.Begin); - using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + if (this.Predictor == TiffPredictor.Horizontal) { - if (this.Predictor == TiffPredictor.Horizontal) - { - HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); - } - - stream.Write(rows); - stream.Flush(); + HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); } - int size = (int)this.memoryStream.Position; - byte[] buffer = this.memoryStream.GetBuffer(); - this.Output.Write(buffer, 0, size); + // Compressed bytes stream straight to the output in fixed segments; the strip byte + // count is measured by the caller from the output position. + using ChunkedWriteStream segmentStream = new(this.Allocator, this.Output.Write); + using ZLibStream stream = new(segmentStream, new ZLibCompressionOptions { CompressionLevel = (int)this.compressionLevel }, true); + stream.Write(rows); } /// diff --git a/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs b/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs deleted file mode 100644 index 64a8092c6..000000000 --- a/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Compression.Zlib; -using SharpAdler32 = ICSharpCode.SharpZipLib.Checksum.Adler32; - -namespace SixLabors.ImageSharp.Benchmarks.General; - -[Config(typeof(Config.Short))] -public class Adler32Benchmark -{ - private byte[] data; - private readonly SharpAdler32 adler = new(); - - [Params(1024, 2048, 4096)] - public int Count { get; set; } - - [GlobalSetup] - public void SetUp() - { - this.data = new byte[this.Count]; - new Random(1).NextBytes(this.data); - } - - [Benchmark(Baseline = true)] - public long SharpZipLibCalculate() - { - this.adler.Reset(); - this.adler.Update(this.data); - return this.adler.Value; - } - - [Benchmark] - public uint SixLaborsCalculate() - { - return Adler32.Calculate(this.data); - } -} - -// ########## 17/05/2020 ########## -// -// | Method | Runtime | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -// |--------------------- |-------------- |------ |------------:|------------:|----------:|------:|--------:|------:|------:|------:|----------:| -// | SharpZipLibCalculate | .NET 4.7.2 | 1024 | 793.18 ns | 775.66 ns | 42.516 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET 4.7.2 | 1024 | 384.86 ns | 15.64 ns | 0.857 ns | 0.49 | 0.03 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 2.1 | 1024 | 790.31 ns | 353.34 ns | 19.368 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 2.1 | 1024 | 465.28 ns | 652.41 ns | 35.761 ns | 0.59 | 0.03 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 3.1 | 1024 | 877.25 ns | 97.89 ns | 5.365 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 3.1 | 1024 | 45.60 ns | 13.28 ns | 0.728 ns | 0.05 | 0.00 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET 4.7.2 | 2048 | 1,537.04 ns | 428.44 ns | 23.484 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET 4.7.2 | 2048 | 849.76 ns | 1,066.34 ns | 58.450 ns | 0.55 | 0.04 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 2.1 | 2048 | 1,616.97 ns | 276.70 ns | 15.167 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 2.1 | 2048 | 790.77 ns | 691.71 ns | 37.915 ns | 0.49 | 0.03 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 3.1 | 2048 | 1,735.11 ns | 1,374.22 ns | 75.325 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 3.1 | 2048 | 87.80 ns | 56.84 ns | 3.116 ns | 0.05 | 0.00 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET 4.7.2 | 4096 | 3,054.53 ns | 796.41 ns | 43.654 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET 4.7.2 | 4096 | 1,538.90 ns | 487.02 ns | 26.695 ns | 0.50 | 0.01 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 2.1 | 4096 | 3,223.48 ns | 32.32 ns | 1.771 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 2.1 | 4096 | 1,547.60 ns | 309.72 ns | 16.977 ns | 0.48 | 0.01 | - | - | - | - | -// | | | | | | | | | | | | | -// | SharpZipLibCalculate | .NET Core 3.1 | 4096 | 3,672.33 ns | 1,095.81 ns | 60.065 ns | 1.00 | 0.00 | - | - | - | - | -// | SixLaborsCalculate | .NET Core 3.1 | 4096 | 159.44 ns | 36.31 ns | 1.990 ns | 0.04 | 0.00 | - | - | - | - | diff --git a/tests/ImageSharp.Tests/Compression/Zlib/ChunkedWriteStreamTests.cs b/tests/ImageSharp.Tests/Compression/Zlib/ChunkedWriteStreamTests.cs new file mode 100644 index 000000000..09b7a46db --- /dev/null +++ b/tests/ImageSharp.Tests/Compression/Zlib/ChunkedWriteStreamTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; + +namespace SixLabors.ImageSharp.Tests.Compression.Zlib; + +public class ChunkedWriteStreamTests +{ + [Theory] + [InlineData(1)] + [InlineData(7)] + [InlineData(64)] + [InlineData(1000)] + public void Write_EmitsFixedLengthSegments_AndPartialTailOnDispose(int writeSize) + { + const int SegmentLength = 64; + byte[] data = new byte[250]; + new Random(42).NextBytes(data); + + List segments = []; + using (ChunkedWriteStream stream = new(Configuration.Default.MemoryAllocator, SegmentLength, segment => segments.Add(segment.ToArray()))) + { + for (int offset = 0; offset < data.Length; offset += writeSize) + { + stream.Write(data, offset, Math.Min(writeSize, data.Length - offset)); + } + + // Nothing but full segments is emitted before disposal. + Assert.Equal(3, segments.Count); + Assert.All(segments, s => Assert.Equal(SegmentLength, s.Length)); + } + + Assert.Equal(4, segments.Count); + Assert.Equal(250 - (3 * SegmentLength), segments[3].Length); + Assert.Equal(data, segments.SelectMany(s => s).ToArray()); + } + + [Fact] + public void Write_ExactMultipleOfSegmentLength_DoesNotEmitEmptyTail() + { + const int SegmentLength = 16; + byte[] data = new byte[SegmentLength * 3]; + + int count = 0; + using (ChunkedWriteStream stream = new(Configuration.Default.MemoryAllocator, SegmentLength, _ => count++)) + { + stream.Write(data); + } + + Assert.Equal(3, count); + } + + [Fact] + public void WriteByte_FillsSegments() + { + const int SegmentLength = 4; + List segments = []; + using (ChunkedWriteStream stream = new(Configuration.Default.MemoryAllocator, SegmentLength, segment => segments.Add(segment.ToArray()))) + { + for (byte i = 0; i < 6; i++) + { + stream.WriteByte(i); + } + } + + Assert.Equal(2, segments.Count); + Assert.Equal(new byte[] { 0, 1, 2, 3 }, segments[0]); + Assert.Equal(new byte[] { 4, 5 }, segments[1]); + } + + [Fact] + public void Flush_DoesNotEmitPartialSegment() + { + int count = 0; + using (ChunkedWriteStream stream = new(Configuration.Default.MemoryAllocator, 16, _ => count++)) + { + stream.Write(new byte[5]); + stream.Flush(); + Assert.Equal(0, count); + } + + Assert.Equal(1, count); + } + + [Fact] + public void Dispose_WithoutWrites_EmitsNothing() + { + int count = 0; + using (ChunkedWriteStream stream = new(Configuration.Default.MemoryAllocator, 16, _ => count++)) + { + } + + Assert.Equal(0, count); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs b/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs deleted file mode 100644 index c2b640a1d..000000000 --- a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Tests.TestUtilities; -using SharpAdler32 = ICSharpCode.SharpZipLib.Checksum.Adler32; - -namespace SixLabors.ImageSharp.Tests.Formats.Png; - -[Trait("Format", "Png")] -public class Adler32Tests -{ - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - public void CalculateAdler_ReturnsCorrectWhenEmpty(uint input) => Assert.Equal(input, Adler32.Calculate(input, default)); - - [Theory] - [InlineData(0)] - [InlineData(8)] - [InlineData(215)] - [InlineData(1024)] - [InlineData(1024 + 15)] - [InlineData(2034)] - [InlineData(4096)] - public void CalculateAdler_MatchesReference(int length) => CalculateAdlerAndCompareToReference(length); - - private static void CalculateAdlerAndCompareToReference(int length) - { - // arrange - byte[] data = GetBuffer(length); - SharpAdler32 adler = new(); - adler.Update(data); - long expected = adler.Value; - - // act - long actual = Adler32.Calculate(data); - - // assert - Assert.Equal(expected, actual); - } - - private static byte[] GetBuffer(int length) - { - byte[] data = new byte[length]; - new Random(1).NextBytes(data); - - return data; - } - - [Fact] - public void RunCalculateAdlerTest_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.AllowAll); - - [Fact] - public void RunCalculateAdlerTest_WithAvxDisabled_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); - - [Fact] - public void RunCalculateAdlerTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.DisableHWIntrinsic); - - private static void RunCalculateAdlerTest() - { - int[] testData = [0, 8, 215, 1024, 1024 + 15, 2034, 4096]; - for (int i = 0; i < testData.Length; i++) - { - CalculateAdlerAndCompareToReference(testData[i]); - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs index cc2faeab7..f36bcd6bd 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; @@ -35,7 +36,7 @@ public class DeflateTiffCompressionTests Stream compressedStream = new MemoryStream(); using (Stream uncompressedStream = new MemoryStream(data), - deflateStream = new ZlibDeflateStream(Configuration.Default.MemoryAllocator, compressedStream, DeflateCompressionLevel.Level6)) + deflateStream = new ZLibStream(compressedStream, new ZLibCompressionOptions { CompressionLevel = (int)DeflateCompressionLevel.Level6 }, true)) { uncompressedStream.CopyTo(deflateStream); }