diff --git a/src/ImageSharp/Common/Helpers/Buffer2DUtils.cs b/src/ImageSharp/Common/Helpers/Buffer2DUtils.cs
index 0c22aa68f..b678e798f 100644
--- a/src/ImageSharp/Common/Helpers/Buffer2DUtils.cs
+++ b/src/ImageSharp/Common/Helpers/Buffer2DUtils.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -62,7 +63,7 @@ namespace SixLabors.ImageSharp
}
///
- /// Computes the sum of vectors in weighted by the kernel weight values.
+ /// Computes the sum of vectors in weighted by the kernel weight values and accumulates the partial results.
///
/// The 1D convolution kernel.
/// The source frame.
@@ -73,16 +74,20 @@ namespace SixLabors.ImageSharp
/// The maximum working area row.
/// The minimum working area column.
/// The maximum working area column.
- public static void Convolve4(
+ /// The weight factor for the real component of the complex pixel values.
+ /// The weight factor for the imaginary component of the complex pixel values.
+ public static void Convolve4AndAccumulatePartials(
Span kernel,
Buffer2D sourceValues,
- Span targetRow,
+ Span targetRow,
int row,
int column,
int minRow,
int maxRow,
int minColumn,
- int maxColumn)
+ int maxColumn,
+ float z,
+ float w)
{
ComplexVector4 vector = default;
int kernelLength = kernel.Length;
@@ -99,7 +104,7 @@ namespace SixLabors.ImageSharp
vector.Sum(Unsafe.Add(ref baseRef, x) * Unsafe.Add(ref sourceRef, offsetX));
}
- targetRow[column] = vector;
+ targetRow[column] += vector.WeightedSum(z, w);
}
}
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
index 3d7510bc2..e1fc9ef1f 100644
--- a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
@@ -22,9 +22,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private bool IsSupportedFileFormat(ReadOnlySpan header)
{
- short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header);
- return header.Length >= this.HeaderSize &&
- (fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray);
+ if (header.Length >= this.HeaderSize)
+ {
+ short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header);
+ return fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray;
+ }
+
+ return false;
}
}
}
diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs
index c75f9465a..1fee4a837 100644
--- a/src/ImageSharp/Formats/Png/PngChunk.cs
+++ b/src/ImageSharp/Formats/Png/PngChunk.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.Memory;
@@ -10,12 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png
///
internal readonly struct PngChunk
{
- public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null, uint crc = 0)
+ public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null)
{
this.Length = length;
this.Type = type;
this.Data = data;
- this.Crc = crc;
}
///
@@ -38,20 +37,12 @@ namespace SixLabors.ImageSharp.Formats.Png
///
public IManagedByteBuffer Data { get; }
- ///
- /// Gets a CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk,
- /// including the chunk type code and chunk data fields, but not including the length field.
- /// The CRC is always present, even for chunks containing no data
- ///
- public uint Crc { get; }
-
///
/// Gets a value indicating whether the given chunk is critical to decoding
///
public bool IsCritical =>
this.Type == PngChunkType.Header ||
this.Type == PngChunkType.Palette ||
- this.Type == PngChunkType.Data ||
- this.Type == PngChunkType.End;
+ this.Type == PngChunkType.Data;
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 037f648f0..b24a5eabd 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -221,7 +221,7 @@ namespace SixLabors.ImageSharp.Formats.Png
if (image is null)
{
- throw new ImageFormatException("PNG Image does not contain a data chunk");
+ PngThrowHelper.ThrowNoData();
}
return image;
@@ -285,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.header.Width == 0 && this.header.Height == 0)
{
- throw new ImageFormatException("PNG Image does not contain a header chunk");
+ PngThrowHelper.ThrowNoHeader();
}
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata);
@@ -407,7 +407,8 @@ namespace SixLabors.ImageSharp.Formats.Png
case PngColorType.RgbWithAlpha:
return this.header.BitDepth * 4;
default:
- throw new NotSupportedException("Unsupported PNG color type");
+ PngThrowHelper.ThrowNotSupportedColor();
+ return -1;
}
}
@@ -528,7 +529,8 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
default:
- throw new ImageFormatException("Unknown filter type.");
+ PngThrowHelper.ThrowUnknownFilter();
+ break;
}
this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata);
@@ -601,7 +603,8 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
default:
- throw new ImageFormatException("Unknown filter type.");
+ PngThrowHelper.ThrowUnknownFilter();
+ break;
}
Span rowSpan = image.GetPixelRowSpan(this.currentRow);
@@ -1119,13 +1122,9 @@ namespace SixLabors.ImageSharp.Formats.Png
chunk = new PngChunk(
length: length,
type: type,
- data: this.ReadChunkData(length),
- crc: this.ReadChunkCrc());
+ data: this.ReadChunkData(length));
- if (chunk.IsCritical)
- {
- this.ValidateChunk(chunk);
- }
+ this.ValidateChunk(chunk);
return true;
}
@@ -1136,6 +1135,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The .
private void ValidateChunk(in PngChunk chunk)
{
+ if (!chunk.IsCritical)
+ {
+ return;
+ }
+
Span chunkType = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type);
@@ -1144,31 +1148,34 @@ namespace SixLabors.ImageSharp.Formats.Png
this.crc.Update(chunkType);
this.crc.Update(chunk.Data.GetSpan());
- if (this.crc.Value != chunk.Crc)
+ uint crc = this.ReadChunkCrc();
+ if (this.crc.Value != crc)
{
string chunkTypeName = Encoding.ASCII.GetString(chunkType);
-
- throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!");
+ PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName);
}
}
///
/// Reads the cycle redundancy chunk from the data.
///
- ///
- /// Thrown if the input stream is not valid or corrupt.
- ///
+ [MethodImpl(InliningOptions.ShortMethod)]
private uint ReadChunkCrc()
{
- return this.currentStream.Read(this.buffer, 0, 4) == 4
- ? BinaryPrimitives.ReadUInt32BigEndian(this.buffer)
- : throw new ImageFormatException("Image stream is not valid!");
+ uint crc = 0;
+ if (this.currentStream.Read(this.buffer, 0, 4) == 4)
+ {
+ crc = BinaryPrimitives.ReadUInt32BigEndian(this.buffer);
+ }
+
+ return crc;
}
///
/// Skips the chunk data and the cycle redundancy chunk read from the data.
///
/// The image format chunk.
+ [MethodImpl(InliningOptions.ShortMethod)]
private void SkipChunkDataAndCrc(in PngChunk chunk)
{
this.currentStream.Skip(chunk.Length);
@@ -1179,6 +1186,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Reads the chunk data from the stream.
///
/// The length of the chunk data to read.
+ [MethodImpl(InliningOptions.ShortMethod)]
private IManagedByteBuffer ReadChunkData(int length)
{
// We rent the buffer here to return it afterwards in Decode()
@@ -1195,11 +1203,20 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Thrown if the input stream is not valid.
///
+ [MethodImpl(InliningOptions.ShortMethod)]
private PngChunkType ReadChunkType()
{
- return this.currentStream.Read(this.buffer, 0, 4) == 4
- ? (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer)
- : throw new ImageFormatException("Invalid PNG data.");
+ if (this.currentStream.Read(this.buffer, 0, 4) == 4)
+ {
+ return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer);
+ }
+ else
+ {
+ PngThrowHelper.ThrowInvalidChunkType();
+
+ // The IDE cannot detect the throw here.
+ return default;
+ }
}
///
@@ -1208,6 +1225,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Whether the length was read.
///
+ [MethodImpl(InliningOptions.ShortMethod)]
private bool TryReadChunkLength(out int result)
{
if (this.currentStream.Read(this.buffer, 0, 4) == 4)
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 09575bb28..19c6af27f 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -699,7 +699,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
using (var memoryStream = new MemoryStream())
{
- using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel))
+ using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
{
deflateStream.Write(textBytes);
}
@@ -790,7 +790,7 @@ namespace SixLabors.ImageSharp.Formats.Png
using (var memoryStream = new MemoryStream())
{
- using (var deflateStream = new ZlibDeflateStream(memoryStream, this.options.CompressionLevel))
+ using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
{
if (this.options.InterlaceMethod == PngInterlaceMode.Adam7)
{
diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
new file mode 100644
index 000000000..00b40c50b
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// Cold path optimizations for throwing png format based exceptions.
+ ///
+ internal static class PngThrowHelper
+ {
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoHeader() => throw new ImageFormatException("PNG Image does not contain a header chunk");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoData() => throw new ImageFormatException("PNG Image does not contain a data chunk");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowInvalidChunkType() => throw new ImageFormatException("Invalid PNG data.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNotSupportedColor() => new NotSupportedException("Unsupported PNG color type");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowUnknownFilter() => throw new ImageFormatException("Unknown filter type.");
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs
index a06983b9e..c4dc82a4d 100644
--- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs
@@ -1,8 +1,9 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
@@ -112,7 +113,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(ReadOnlySpan data)
{
- // (By Per Bothner)
+ ref byte dataRef = ref MemoryMarshal.GetReference(data);
uint s1 = this.checksum & 0xFFFF;
uint s2 = this.checksum >> 16;
@@ -133,8 +134,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
count -= n;
while (--n >= 0)
{
- s1 = s1 + (uint)(data[offset++] & 0xff);
- s2 = s2 + s1;
+ s1 += Unsafe.Add(ref dataRef, offset++);
+ s2 += s1;
}
s1 %= Base;
@@ -144,4 +145,4 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.checksum = (s2 << 16) | s1;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs
index d1588c384..77355e908 100644
--- a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/Crc32.cs
@@ -1,8 +1,9 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
@@ -141,9 +142,10 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
this.crc ^= CrcSeed;
+ ref uint crcTableRef = ref MemoryMarshal.GetReference(CrcTable.AsSpan());
for (int i = 0; i < data.Length; i++)
{
- this.crc = CrcTable[(this.crc ^ data[i]) & 0xFF] ^ (this.crc >> 8);
+ this.crc = Unsafe.Add(ref crcTableRef, (int)((this.crc ^ data[i]) & 0xFF)) ^ (this.crc >> 8);
}
this.crc ^= CrcSeed;
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs
new file mode 100644
index 000000000..5f62b13c7
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ internal static class DeflateThrowHelper
+ {
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNull(string name) => throw new ArgumentNullException(name);
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name);
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input.");
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs
new file mode 100644
index 000000000..6c4ea44d1
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs
@@ -0,0 +1,295 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// This class compresses input with the deflate algorithm described in RFC 1951.
+ /// It has several compression levels and three different strategies described below.
+ ///
+ internal sealed class Deflater : IDisposable
+ {
+ ///
+ /// The best and slowest compression level. This tries to find very
+ /// long and distant string repetitions.
+ ///
+ public const int BestCompression = 9;
+
+ ///
+ /// The worst but fastest compression level.
+ ///
+ public const int BestSpeed = 1;
+
+ ///
+ /// The default compression level.
+ ///
+ public const int DefaultCompression = -1;
+
+ ///
+ /// This level won't compress at all but output uncompressed blocks.
+ ///
+ public const int NoCompression = 0;
+
+ ///
+ /// The compression method. This is the only method supported so far.
+ /// There is no need to use this constant at all.
+ ///
+ public const int Deflated = 8;
+
+ ///
+ /// Compression level.
+ ///
+ private int level;
+
+ ///
+ /// The current state.
+ ///
+ private int state;
+
+ private DeflaterEngine engine;
+ private bool isDisposed;
+
+ private const int IsFlushing = 0x04;
+ private const int IsFinishing = 0x08;
+ private const int BusyState = 0x10;
+ private const int FlushingState = 0x14;
+ private const int FinishingState = 0x1c;
+ private const int FinishedState = 0x1e;
+ private const int ClosedState = 0x7f;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator to use for buffer allocations.
+ /// The compression level, a value between NoCompression and BestCompression.
+ ///
+ /// if level is out of range.
+ public Deflater(MemoryAllocator memoryAllocator, int level)
+ {
+ if (level == DefaultCompression)
+ {
+ level = 6;
+ }
+ else if (level < NoCompression || level > BestCompression)
+ {
+ throw new ArgumentOutOfRangeException(nameof(level));
+ }
+
+ // TODO: Possibly provide DeflateStrategy as an option.
+ this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default);
+
+ this.SetLevel(level);
+ this.Reset();
+ }
+
+ ///
+ /// Compression Level as an enum for safer use
+ ///
+ public enum CompressionLevel
+ {
+ ///
+ /// The best and slowest compression level. This tries to find very
+ /// long and distant string repetitions.
+ ///
+ BestCompression = Deflater.BestCompression,
+
+ ///
+ /// The worst but fastest compression level.
+ ///
+ BestSpeed = Deflater.BestSpeed,
+
+ ///
+ /// The default compression level.
+ ///
+ DefaultCompression = Deflater.DefaultCompression,
+
+ ///
+ /// This level won't compress at all but output uncompressed blocks.
+ ///
+ NoCompression = Deflater.NoCompression,
+
+ ///
+ /// The compression method. This is the only method supported so far.
+ /// There is no need to use this constant at all.
+ ///
+ Deflated = Deflater.Deflated
+ }
+
+ ///
+ /// Gets a value indicating whetherthe stream was finished and no more output bytes
+ /// are available.
+ ///
+ public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed;
+
+ ///
+ /// Gets a value indicating whether the input buffer is empty.
+ /// You should then call setInput().
+ /// NOTE: This method can also return true when the stream
+ /// was finished.
+ ///
+ public bool IsNeedingInput => this.engine.NeedsInput();
+
+ ///
+ /// Resets the deflater. The deflater acts afterwards as if it was
+ /// just created with the same compression level and strategy as it
+ /// had before.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Reset()
+ {
+ this.state = BusyState;
+ this.engine.Pending.Reset();
+ this.engine.Reset();
+ }
+
+ ///
+ /// Flushes the current input block. Further calls to Deflate() will
+ /// produce enough output to inflate everything in the current input
+ /// block. It is used by DeflaterOutputStream to implement Flush().
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Flush() => this.state |= IsFlushing;
+
+ ///
+ /// Finishes the deflater with the current input block. It is an error
+ /// to give more input after this method was called. This method must
+ /// be called to force all bytes to be flushed.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Finish() => this.state |= IsFlushing | IsFinishing;
+
+ ///
+ /// Sets the data which should be compressed next. This should be
+ /// only called when needsInput indicates that more input is needed.
+ /// The given byte array should not be changed, before needsInput() returns
+ /// true again.
+ ///
+ /// The buffer containing the input data.
+ /// The start of the data.
+ /// The number of data bytes of input.
+ ///
+ /// if the buffer was finished or if previous input is still pending.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void SetInput(byte[] input, int offset, int count)
+ {
+ if ((this.state & IsFinishing) != 0)
+ {
+ DeflateThrowHelper.ThrowAlreadyFinished();
+ }
+
+ this.engine.SetInput(input, offset, count);
+ }
+
+ ///
+ /// Sets the compression level. There is no guarantee of the exact
+ /// position of the change, but if you call this when needsInput is
+ /// true the change of compression level will occur somewhere near
+ /// before the end of the so far given input.
+ ///
+ ///
+ /// the new compression level.
+ ///
+ public void SetLevel(int level)
+ {
+ if (level == DefaultCompression)
+ {
+ level = 6;
+ }
+ else if (level < NoCompression || level > BestCompression)
+ {
+ throw new ArgumentOutOfRangeException(nameof(level));
+ }
+
+ if (this.level != level)
+ {
+ this.level = level;
+ this.engine.SetLevel(level);
+ }
+ }
+
+ ///
+ /// Deflates the current input block to the given array.
+ ///
+ /// Buffer to store the compressed data.
+ /// Offset into the output array.
+ /// The maximum number of bytes that may be stored.
+ ///
+ /// The number of compressed bytes added to the output, or 0 if either
+ /// or returns true or length is zero.
+ ///
+ public int Deflate(byte[] output, int offset, int length)
+ {
+ int origLength = length;
+
+ if (this.state == ClosedState)
+ {
+ DeflateThrowHelper.ThrowAlreadyClosed();
+ }
+
+ while (true)
+ {
+ int count = this.engine.Pending.Flush(output, offset, length);
+ offset += count;
+ length -= count;
+
+ if (length == 0 || this.state == FinishedState)
+ {
+ break;
+ }
+
+ if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0))
+ {
+ switch (this.state)
+ {
+ case BusyState:
+ // We need more input now
+ return origLength - length;
+
+ case FlushingState:
+ if (this.level != NoCompression)
+ {
+ // We have to supply some lookahead. 8 bit lookahead
+ // is needed by the zlib inflater, and we must fill
+ // the next byte, so that all bits are flushed.
+ int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7);
+ while (neededbits > 0)
+ {
+ // Write a static tree block consisting solely of an EOF:
+ this.engine.Pending.WriteBits(2, 10);
+ neededbits -= 10;
+ }
+ }
+
+ this.state = BusyState;
+ break;
+
+ case FinishingState:
+ this.engine.Pending.AlignToByte();
+ this.state = FinishedState;
+ break;
+ }
+ }
+ }
+
+ return origLength - length;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.engine.Dispose();
+ this.engine = null;
+ this.isDisposed = true;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs
new file mode 100644
index 000000000..67e8c6900
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// This class contains constants used for deflation.
+ ///
+ public static class DeflaterConstants
+ {
+ ///
+ /// Set to true to enable debugging
+ ///
+ public const bool DEBUGGING = false;
+
+ ///
+ /// Written to Zip file to identify a stored block
+ ///
+ public const int STORED_BLOCK = 0;
+
+ ///
+ /// Identifies static tree in Zip file
+ ///
+ public const int STATIC_TREES = 1;
+
+ ///
+ /// Identifies dynamic tree in Zip file
+ ///
+ public const int DYN_TREES = 2;
+
+ ///
+ /// Header flag indicating a preset dictionary for deflation
+ ///
+ public const int PRESET_DICT = 0x20;
+
+ ///
+ /// Sets internal buffer sizes for Huffman encoding
+ ///
+ public const int DEFAULT_MEM_LEVEL = 8;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int MAX_MATCH = 258;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int MIN_MATCH = 3;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int MAX_WBITS = 15;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int WSIZE = 1 << MAX_WBITS;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int WMASK = WSIZE - 1;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int HASH_SIZE = 1 << HASH_BITS;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int HASH_MASK = HASH_SIZE - 1;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8);
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5);
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int DEFLATE_STORED = 0;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int DEFLATE_FAST = 1;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public const int DEFLATE_SLOW = 2;
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 };
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 };
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 };
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 };
+
+ ///
+ /// Internal compression engine constant
+ ///
+ public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 };
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
new file mode 100644
index 000000000..0163eec0b
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
@@ -0,0 +1,859 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// Strategies for deflater
+ ///
+ public enum DeflateStrategy
+ {
+ ///
+ /// The default strategy
+ ///
+ Default = 0,
+
+ ///
+ /// This strategy will only allow longer string repetitions. It is
+ /// useful for random data with a small character set.
+ ///
+ Filtered = 1,
+
+ ///
+ /// This strategy will not look for string repetitions at all. It
+ /// only encodes with Huffman trees (which means, that more common
+ /// characters get a smaller encoding.
+ ///
+ HuffmanOnly = 2
+ }
+
+ // DEFLATE ALGORITHM:
+ //
+ // The uncompressed stream is inserted into the window array. When
+ // the window array is full the first half is thrown away and the
+ // second half is copied to the beginning.
+ //
+ // The head array is a hash table. Three characters build a hash value
+ // and they the value points to the corresponding index in window of
+ // the last string with this hash. The prev array implements a
+ // linked list of matches with the same hash: prev[index & WMASK] points
+ // to the previous index with the same hash.
+ //
+
+ ///
+ /// Low level compression engine for deflate algorithm which uses a 32K sliding window
+ /// with secondary compression from Huffman/Shannon-Fano codes.
+ ///
+ internal sealed unsafe class DeflaterEngine : IDisposable
+ {
+ private const int TooFar = 4096;
+
+ // Hash index of string to be inserted
+ private int insertHashIndex;
+
+ private int matchStart;
+
+ // Length of best match
+ private int matchLen;
+
+ // Set if previous match exists
+ private bool prevAvailable;
+
+ private int blockStart;
+
+ ///
+ /// Points to the current character in the window.
+ ///
+ private int strstart;
+
+ ///
+ /// lookahead is the number of characters starting at strstart in
+ /// window that are valid.
+ /// So window[strstart] until window[strstart+lookahead-1] are valid
+ /// characters.
+ ///
+ private int lookahead;
+
+ ///
+ /// The current compression function.
+ ///
+ private int compressionFunction;
+
+ ///
+ /// The input data for compression.
+ ///
+ private byte[] inputBuf;
+
+ ///
+ /// The offset into inputBuf, where input data starts.
+ ///
+ private int inputOff;
+
+ ///
+ /// The end offset of the input data.
+ ///
+ private int inputEnd;
+
+ private readonly DeflateStrategy strategy;
+ private DeflaterHuffman huffman;
+ private bool isDisposed;
+
+ ///
+ /// Hashtable, hashing three characters to an index for window, so
+ /// that window[index]..window[index+2] have this hash code.
+ /// Note that the array should really be unsigned short, so you need
+ /// to and the values with 0xFFFF.
+ ///
+ private IMemoryOwner headMemoryOwner;
+ private MemoryHandle headMemoryHandle;
+ private readonly Memory head;
+ private readonly short* pinnedHeadPointer;
+
+ ///
+ /// prev[index & WMASK] points to the previous index that has the
+ /// same hash code as the string starting at index. This way
+ /// entries with the same hash code are in a linked list.
+ /// Note that the array should really be unsigned short, so you need
+ /// to and the values with 0xFFFF.
+ ///
+ private IMemoryOwner prevMemoryOwner;
+ private MemoryHandle prevMemoryHandle;
+ private readonly Memory prev;
+ private readonly short* pinnedPrevPointer;
+
+ ///
+ /// This array contains the part of the uncompressed stream that
+ /// is of relevance. The current character is indexed by strstart.
+ ///
+ private IManagedByteBuffer windowMemoryOwner;
+ private MemoryHandle windowMemoryHandle;
+ private readonly byte[] window;
+ private readonly byte* pinnedWindowPointer;
+
+ private int maxChain;
+ private int maxLazy;
+ private int niceLength;
+ private int goodLength;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator to use for buffer allocations.
+ /// The deflate strategy to use.
+ public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy)
+ {
+ this.huffman = new DeflaterHuffman(memoryAllocator);
+ this.Pending = this.huffman.Pending;
+ this.strategy = strategy;
+
+ // Create pinned pointers to the various buffers to allow indexing
+ // without bounds checks.
+ this.windowMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(2 * DeflaterConstants.WSIZE);
+ this.window = this.windowMemoryOwner.Array;
+ this.windowMemoryHandle = this.windowMemoryOwner.Memory.Pin();
+ this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer;
+
+ this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE);
+ this.head = this.headMemoryOwner.Memory;
+ this.headMemoryHandle = this.headMemoryOwner.Memory.Pin();
+ this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer;
+
+ this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE);
+ this.prev = this.prevMemoryOwner.Memory;
+ this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin();
+ this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer;
+
+ // We start at index 1, to avoid an implementation deficiency, that
+ // we cannot build a repeat pattern at index 0.
+ this.blockStart = this.strstart = 1;
+ }
+
+ ///
+ /// Gets the pending buffer to use.
+ ///
+ public DeflaterPendingBuffer Pending { get; }
+
+ ///
+ /// Deflate drives actual compression of data
+ ///
+ /// True to flush input buffers
+ /// Finish deflation with the current input.
+ /// Returns true if progress has been made.
+ public bool Deflate(bool flush, bool finish)
+ {
+ bool progress = false;
+ do
+ {
+ this.FillWindow();
+ bool canFlush = flush && (this.inputOff == this.inputEnd);
+
+ switch (this.compressionFunction)
+ {
+ case DeflaterConstants.DEFLATE_STORED:
+ progress = this.DeflateStored(canFlush, finish);
+ break;
+
+ case DeflaterConstants.DEFLATE_FAST:
+ progress = this.DeflateFast(canFlush, finish);
+ break;
+
+ case DeflaterConstants.DEFLATE_SLOW:
+ progress = this.DeflateSlow(canFlush, finish);
+ break;
+
+ default:
+ DeflateThrowHelper.ThrowUnknownCompression();
+ break;
+ }
+ }
+ while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made
+ return progress;
+ }
+
+ ///
+ /// Sets input data to be deflated. Should only be called when
+ /// returns true
+ ///
+ /// The buffer containing input data.
+ /// The offset of the first byte of data.
+ /// The number of bytes of data to use as input.
+ public void SetInput(byte[] buffer, int offset, int count)
+ {
+ if (buffer is null)
+ {
+ DeflateThrowHelper.ThrowNull(nameof(buffer));
+ }
+
+ if (offset < 0)
+ {
+ DeflateThrowHelper.ThrowOutOfRange(nameof(offset));
+ }
+
+ if (count < 0)
+ {
+ DeflateThrowHelper.ThrowOutOfRange(nameof(count));
+ }
+
+ if (this.inputOff < this.inputEnd)
+ {
+ DeflateThrowHelper.ThrowNotProcessed();
+ }
+
+ int end = offset + count;
+
+ // We want to throw an ArgumentOutOfRangeException early.
+ // The check is very tricky: it also handles integer wrap around.
+ if ((offset > end) || (end > buffer.Length))
+ {
+ DeflateThrowHelper.ThrowOutOfRange(nameof(count));
+ }
+
+ this.inputBuf = buffer;
+ this.inputOff = offset;
+ this.inputEnd = end;
+ }
+
+ ///
+ /// Determines if more input is needed.
+ ///
+ /// Return true if input is needed via SetInput
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool NeedsInput() => this.inputEnd == this.inputOff;
+
+ ///
+ /// Reset internal state
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Reset()
+ {
+ this.huffman.Reset();
+ this.blockStart = this.strstart = 1;
+ this.lookahead = 0;
+ this.prevAvailable = false;
+ this.matchLen = DeflaterConstants.MIN_MATCH - 1;
+ this.head.Span.Slice(0, DeflaterConstants.HASH_SIZE).Clear();
+ this.prev.Span.Slice(0, DeflaterConstants.WSIZE).Clear();
+ }
+
+ ///
+ /// Set the deflate level (0-9)
+ ///
+ /// The value to set the level to.
+ public void SetLevel(int level)
+ {
+ if ((level < 0) || (level > 9))
+ {
+ DeflateThrowHelper.ThrowOutOfRange(nameof(level));
+ }
+
+ this.goodLength = DeflaterConstants.GOOD_LENGTH[level];
+ this.maxLazy = DeflaterConstants.MAX_LAZY[level];
+ this.niceLength = DeflaterConstants.NICE_LENGTH[level];
+ this.maxChain = DeflaterConstants.MAX_CHAIN[level];
+
+ if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction)
+ {
+ switch (this.compressionFunction)
+ {
+ case DeflaterConstants.DEFLATE_STORED:
+ if (this.strstart > this.blockStart)
+ {
+ this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.blockStart = this.strstart;
+ }
+
+ this.UpdateHash();
+ break;
+
+ case DeflaterConstants.DEFLATE_FAST:
+ if (this.strstart > this.blockStart)
+ {
+ this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.blockStart = this.strstart;
+ }
+
+ break;
+
+ case DeflaterConstants.DEFLATE_SLOW:
+ if (this.prevAvailable)
+ {
+ this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF);
+ }
+
+ if (this.strstart > this.blockStart)
+ {
+ this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.blockStart = this.strstart;
+ }
+
+ this.prevAvailable = false;
+ this.matchLen = DeflaterConstants.MIN_MATCH - 1;
+ break;
+ }
+
+ this.compressionFunction = DeflaterConstants.COMPR_FUNC[level];
+ }
+ }
+
+ ///
+ /// Fill the window
+ ///
+ public void FillWindow()
+ {
+ // If the window is almost full and there is insufficient lookahead,
+ // move the upper half to the lower one to make room in the upper half.
+ if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST)
+ {
+ this.SlideWindow();
+ }
+
+ // If there is not enough lookahead, but still some input left, read in the input.
+ if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd)
+ {
+ int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart;
+
+ if (more > this.inputEnd - this.inputOff)
+ {
+ more = this.inputEnd - this.inputOff;
+ }
+
+ Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more);
+
+ this.inputOff += more;
+ this.lookahead += more;
+ }
+
+ if (this.lookahead >= DeflaterConstants.MIN_MATCH)
+ {
+ this.UpdateHash();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.huffman.Dispose();
+
+ this.windowMemoryHandle.Dispose();
+ this.windowMemoryOwner.Dispose();
+
+ this.headMemoryHandle.Dispose();
+ this.headMemoryOwner.Dispose();
+
+ this.prevMemoryHandle.Dispose();
+ this.prevMemoryOwner.Dispose();
+
+ this.windowMemoryOwner = null;
+ this.headMemoryOwner = null;
+ this.prevMemoryOwner = null;
+ this.huffman = null;
+
+ this.isDisposed = true;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private void UpdateHash()
+ {
+ byte* pinned = this.pinnedWindowPointer;
+ this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1];
+ }
+
+ ///
+ /// Inserts the current string in the head hash and returns the previous
+ /// value for this hash.
+ ///
+ /// The previous hash value
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private int InsertString()
+ {
+ short match;
+ int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK;
+
+ short* pinnedHead = this.pinnedHeadPointer;
+ this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash];
+ pinnedHead[hash] = unchecked((short)this.strstart);
+ this.insertHashIndex = hash;
+ return match & 0xFFFF;
+ }
+
+ private void SlideWindow()
+ {
+ Unsafe.CopyBlockUnaligned(ref this.window[0], ref this.window[DeflaterConstants.WSIZE], DeflaterConstants.WSIZE);
+ this.matchStart -= DeflaterConstants.WSIZE;
+ this.strstart -= DeflaterConstants.WSIZE;
+ this.blockStart -= DeflaterConstants.WSIZE;
+
+ // Slide the hash table (could be avoided with 32 bit values
+ // at the expense of memory usage).
+ short* pinnedHead = this.pinnedHeadPointer;
+ for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i)
+ {
+ int m = pinnedHead[i] & 0xFFFF;
+ pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0);
+ }
+
+ // Slide the prev table.
+ short* pinnedPrev = this.pinnedPrevPointer;
+ for (int i = 0; i < DeflaterConstants.WSIZE; i++)
+ {
+ int m = pinnedPrev[i] & 0xFFFF;
+ pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0);
+ }
+ }
+
+ ///
+ ///
+ /// Find the best (longest) string in the window matching the
+ /// string starting at strstart.
+ ///
+ ///
+ /// Preconditions:
+ ///
+ /// strstart + DeflaterConstants.MAX_MATCH <= window.length.
+ ///
+ ///
+ /// The current match.
+ /// True if a match greater than the minimum length is found
+ private bool FindLongestMatch(int curMatch)
+ {
+ int match;
+ int scan = this.strstart;
+
+ // scanMax is the highest position that we can look at
+ int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1;
+ int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0);
+
+ int chainLength = this.maxChain;
+ int niceLength = Math.Min(this.niceLength, this.lookahead);
+
+ int matchStrt = this.matchStart;
+ this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1);
+ int matchLength = this.matchLen;
+
+ if (scan + matchLength > scanMax)
+ {
+ return false;
+ }
+
+ byte* pinnedWindow = this.pinnedWindowPointer;
+ int scanStart = this.strstart;
+ byte scanEnd1 = pinnedWindow[scan + matchLength - 1];
+ byte scanEnd = pinnedWindow[scan + matchLength];
+
+ // Do not waste too much time if we already have a good match:
+ if (matchLength >= this.goodLength)
+ {
+ chainLength >>= 2;
+ }
+
+ short* pinnedPrev = this.pinnedPrevPointer;
+ do
+ {
+ match = curMatch;
+ scan = scanStart;
+
+ if (pinnedWindow[match + matchLength] != scanEnd
+ || pinnedWindow[match + matchLength - 1] != scanEnd1
+ || pinnedWindow[match] != pinnedWindow[scan]
+ || pinnedWindow[++match] != pinnedWindow[++scan])
+ {
+ continue;
+ }
+
+ // scan is set to strstart+1 and the comparison passed, so
+ // scanMax - scan is the maximum number of bytes we can compare.
+ // below we compare 8 bytes at a time, so first we compare
+ // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8
+ // n & (8 - 1) == n % 8.
+ switch ((scanMax - scan) & 7)
+ {
+ case 1:
+ if (pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 2:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 3:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 4:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 5:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 6:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+
+ case 7:
+ if (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match])
+ {
+ break;
+ }
+
+ break;
+ }
+
+ if (pinnedWindow[scan] == pinnedWindow[match])
+ {
+ // We check for insufficient lookahead only every 8th comparison;
+ // the 256th check will be made at strstart + 258 unless lookahead is
+ // exhausted first.
+ do
+ {
+ if (scan == scanMax)
+ {
+ ++scan; // advance to first position not matched
+ ++match;
+
+ break;
+ }
+ }
+ while (pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]
+ && pinnedWindow[++scan] == pinnedWindow[++match]);
+ }
+
+ if (scan - scanStart > matchLength)
+ {
+ matchStrt = curMatch;
+ matchLength = scan - scanStart;
+
+ if (matchLength >= niceLength)
+ {
+ break;
+ }
+
+ scanEnd1 = pinnedWindow[scan - 1];
+ scanEnd = pinnedWindow[scan];
+ }
+ }
+ while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0);
+
+ this.matchStart = matchStrt;
+ this.matchLen = matchLength;
+ return matchLength >= DeflaterConstants.MIN_MATCH;
+ }
+
+ private bool DeflateStored(bool flush, bool finish)
+ {
+ if (!flush && (this.lookahead == 0))
+ {
+ return false;
+ }
+
+ this.strstart += this.lookahead;
+ this.lookahead = 0;
+
+ int storedLength = this.strstart - this.blockStart;
+
+ if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full
+ (this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window
+ flush)
+ {
+ bool lastBlock = finish;
+ if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE)
+ {
+ storedLength = DeflaterConstants.MAX_BLOCK_SIZE;
+ lastBlock = false;
+ }
+
+ this.huffman.FlushStoredBlock(this.window, this.blockStart, storedLength, lastBlock);
+ this.blockStart += storedLength;
+ return !(lastBlock || storedLength == 0);
+ }
+
+ return true;
+ }
+
+ private bool DeflateFast(bool flush, bool finish)
+ {
+ if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush)
+ {
+ return false;
+ }
+
+ while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush)
+ {
+ if (this.lookahead == 0)
+ {
+ // We are flushing everything
+ this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish);
+ this.blockStart = this.strstart;
+ return false;
+ }
+
+ if (this.strstart > (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD)
+ {
+ // slide window, as FindLongestMatch needs this.
+ // This should only happen when flushing and the window
+ // is almost full.
+ this.SlideWindow();
+ }
+
+ int hashHead;
+ if (this.lookahead >= DeflaterConstants.MIN_MATCH &&
+ (hashHead = this.InsertString()) != 0 &&
+ this.strategy != DeflateStrategy.HuffmanOnly &&
+ this.strstart - hashHead <= DeflaterConstants.MAX_DIST &&
+ this.FindLongestMatch(hashHead))
+ {
+ // longestMatch sets matchStart and matchLen
+ bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen);
+
+ this.lookahead -= this.matchLen;
+ if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH)
+ {
+ while (--this.matchLen > 0)
+ {
+ ++this.strstart;
+ this.InsertString();
+ }
+
+ ++this.strstart;
+ }
+ else
+ {
+ this.strstart += this.matchLen;
+ if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1)
+ {
+ this.UpdateHash();
+ }
+ }
+
+ this.matchLen = DeflaterConstants.MIN_MATCH - 1;
+ if (!full)
+ {
+ continue;
+ }
+ }
+ else
+ {
+ // No match found
+ this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff);
+ ++this.strstart;
+ --this.lookahead;
+ }
+
+ if (this.huffman.IsFull())
+ {
+ bool lastBlock = finish && (this.lookahead == 0);
+ this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, lastBlock);
+ this.blockStart = this.strstart;
+ return !lastBlock;
+ }
+ }
+
+ return true;
+ }
+
+ private bool DeflateSlow(bool flush, bool finish)
+ {
+ if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush)
+ {
+ return false;
+ }
+
+ while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush)
+ {
+ if (this.lookahead == 0)
+ {
+ if (this.prevAvailable)
+ {
+ this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff);
+ }
+
+ this.prevAvailable = false;
+
+ // We are flushing everything
+ this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish);
+ this.blockStart = this.strstart;
+ return false;
+ }
+
+ if (this.strstart >= (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD)
+ {
+ // slide window, as FindLongestMatch needs this.
+ // This should only happen when flushing and the window
+ // is almost full.
+ this.SlideWindow();
+ }
+
+ int prevMatch = this.matchStart;
+ int prevLen = this.matchLen;
+ if (this.lookahead >= DeflaterConstants.MIN_MATCH)
+ {
+ int hashHead = this.InsertString();
+
+ if (this.strategy != DeflateStrategy.HuffmanOnly &&
+ hashHead != 0 &&
+ this.strstart - hashHead <= DeflaterConstants.MAX_DIST &&
+ this.FindLongestMatch(hashHead))
+ {
+ // longestMatch sets matchStart and matchLen
+ // Discard match if too small and too far away
+ if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar)))
+ {
+ this.matchLen = DeflaterConstants.MIN_MATCH - 1;
+ }
+ }
+ }
+
+ // previous match was better
+ if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen))
+ {
+ this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen);
+ prevLen -= 2;
+ do
+ {
+ this.strstart++;
+ this.lookahead--;
+ if (this.lookahead >= DeflaterConstants.MIN_MATCH)
+ {
+ this.InsertString();
+ }
+ }
+ while (--prevLen > 0);
+
+ this.strstart++;
+ this.lookahead--;
+ this.prevAvailable = false;
+ this.matchLen = DeflaterConstants.MIN_MATCH - 1;
+ }
+ else
+ {
+ if (this.prevAvailable)
+ {
+ this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff);
+ }
+
+ this.prevAvailable = true;
+ this.strstart++;
+ this.lookahead--;
+ }
+
+ if (this.huffman.IsFull())
+ {
+ int len = this.strstart - this.blockStart;
+ if (this.prevAvailable)
+ {
+ len--;
+ }
+
+ bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable;
+ this.huffman.FlushBlock(this.window, this.blockStart, len, lastBlock);
+ this.blockStart += len;
+ return !lastBlock;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
new file mode 100644
index 000000000..96ff6b657
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
@@ -0,0 +1,949 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// Performs Deflate Huffman encoding.
+ ///
+ internal sealed unsafe class DeflaterHuffman : IDisposable
+ {
+ private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6);
+
+ // The number of literal codes.
+ private const int LiteralNumber = 286;
+
+ // Number of distance codes
+ private const int DistanceNumber = 30;
+
+ // Number of codes used to transfer bit lengths
+ private const int BitLengthNumber = 19;
+
+ // Repeat previous bit length 3-6 times (2 bits of repeat count)
+ private const int Repeat3To6 = 16;
+
+ // Repeat a zero length 3-10 times (3 bits of repeat count)
+ private const int Repeat3To10 = 17;
+
+ // Repeat a zero length 11-138 times (7 bits of repeat count)
+ private const int Repeat11To138 = 18;
+
+ private const int EofSymbol = 256;
+
+ // The lengths of the bit length codes are sent in order of decreasing
+ // probability, to avoid transmitting the lengths for unused bit length codes.
+ private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
+
+ private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
+
+ private static readonly short[] StaticLCodes;
+ private static readonly byte[] StaticLLength;
+ private static readonly short[] StaticDCodes;
+ private static readonly byte[] StaticDLength;
+
+ private Tree literalTree;
+ private Tree distTree;
+ private Tree blTree;
+
+ // Buffer for distances
+ private readonly IMemoryOwner distanceManagedBuffer;
+ private readonly short* pinnedDistanceBuffer;
+ private MemoryHandle distanceBufferHandle;
+
+ private readonly IMemoryOwner literalManagedBuffer;
+ private readonly short* pinnedLiteralBuffer;
+ private MemoryHandle literalBufferHandle;
+
+ private int lastLiteral;
+ private int extraBits;
+ private bool isDisposed;
+
+ // TODO: These should be pre-generated array/readonlyspans.
+ static DeflaterHuffman()
+ {
+ // See RFC 1951 3.2.6
+ // Literal codes
+ StaticLCodes = new short[LiteralNumber];
+ StaticLLength = new byte[LiteralNumber];
+
+ int i = 0;
+ while (i < 144)
+ {
+ StaticLCodes[i] = BitReverse((0x030 + i) << 8);
+ StaticLLength[i++] = 8;
+ }
+
+ while (i < 256)
+ {
+ StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7);
+ StaticLLength[i++] = 9;
+ }
+
+ while (i < 280)
+ {
+ StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9);
+ StaticLLength[i++] = 7;
+ }
+
+ while (i < LiteralNumber)
+ {
+ StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8);
+ StaticLLength[i++] = 8;
+ }
+
+ // Distance codes
+ StaticDCodes = new short[DistanceNumber];
+ StaticDLength = new byte[DistanceNumber];
+ for (i = 0; i < DistanceNumber; i++)
+ {
+ StaticDCodes[i] = BitReverse(i << 11);
+ StaticDLength[i] = 5;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator to use for buffer allocations.
+ public DeflaterHuffman(MemoryAllocator memoryAllocator)
+ {
+ this.Pending = new DeflaterPendingBuffer(memoryAllocator);
+
+ this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15);
+ this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15);
+ this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7);
+
+ this.distanceManagedBuffer = memoryAllocator.Allocate(BufferSize);
+ this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin();
+ this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer;
+
+ this.literalManagedBuffer = memoryAllocator.Allocate(BufferSize);
+ this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin();
+ this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
+ }
+
+ ///
+ /// Gets the pending buffer to use.
+ ///
+ public DeflaterPendingBuffer Pending { get; private set; }
+
+ ///
+ /// Reset internal state
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Reset()
+ {
+ this.lastLiteral = 0;
+ this.extraBits = 0;
+ this.literalTree.Reset();
+ this.distTree.Reset();
+ this.blTree.Reset();
+ }
+
+ ///
+ /// Write all trees to pending buffer
+ ///
+ /// The number/rank of treecodes to send.
+ public void SendAllTrees(int blTreeCodes)
+ {
+ this.blTree.BuildCodes();
+ this.literalTree.BuildCodes();
+ this.distTree.BuildCodes();
+ this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5);
+ this.Pending.WriteBits(this.distTree.NumCodes - 1, 5);
+ this.Pending.WriteBits(blTreeCodes - 4, 4);
+ for (int rank = 0; rank < blTreeCodes; rank++)
+ {
+ this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3);
+ }
+
+ this.literalTree.WriteTree(this.Pending, this.blTree);
+ this.distTree.WriteTree(this.Pending, this.blTree);
+ }
+
+ ///
+ /// Compress current buffer writing data to pending buffer
+ ///
+ public void CompressBlock()
+ {
+ DeflaterPendingBuffer pendingBuffer = this.Pending;
+ short* pinnedDistance = this.pinnedDistanceBuffer;
+ short* pinnedLiteral = this.pinnedLiteralBuffer;
+
+ for (int i = 0; i < this.lastLiteral; i++)
+ {
+ int litlen = pinnedLiteral[i] & 0xFF;
+ int dist = pinnedDistance[i];
+ if (dist-- != 0)
+ {
+ int lc = Lcode(litlen);
+ this.literalTree.WriteSymbol(pendingBuffer, lc);
+
+ int bits = (lc - 261) / 4;
+ if (bits > 0 && bits <= 5)
+ {
+ this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits);
+ }
+
+ int dc = Dcode(dist);
+ this.distTree.WriteSymbol(pendingBuffer, dc);
+
+ bits = (dc >> 1) - 1;
+ if (bits > 0)
+ {
+ this.Pending.WriteBits(dist & ((1 << bits) - 1), bits);
+ }
+ }
+ else
+ {
+ this.literalTree.WriteSymbol(pendingBuffer, litlen);
+ }
+ }
+
+ this.literalTree.WriteSymbol(pendingBuffer, EofSymbol);
+ }
+
+ ///
+ /// Flush block to output with no compression
+ ///
+ /// Data to write
+ /// Index of first byte to write
+ /// Count of bytes to write
+ /// True if this is the last block
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock)
+ {
+ this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3);
+ this.Pending.AlignToByte();
+ this.Pending.WriteShort(storedLength);
+ this.Pending.WriteShort(~storedLength);
+ this.Pending.WriteBlock(stored, storedOffset, storedLength);
+ this.Reset();
+ }
+
+ ///
+ /// Flush block to output with compression
+ ///
+ /// Data to flush
+ /// Index of first byte to flush
+ /// Count of bytes to flush
+ /// True if this is the last block
+ public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock)
+ {
+ this.literalTree.Frequencies[EofSymbol]++;
+
+ // Build trees
+ this.literalTree.BuildTree();
+ this.distTree.BuildTree();
+
+ // Calculate bitlen frequency
+ this.literalTree.CalcBLFreq(this.blTree);
+ this.distTree.CalcBLFreq(this.blTree);
+
+ // Build bitlen tree
+ this.blTree.BuildTree();
+
+ int blTreeCodes = 4;
+ for (int i = 18; i > blTreeCodes; i--)
+ {
+ if (this.blTree.Length[BitLengthOrder[i]] > 0)
+ {
+ blTreeCodes = i + 1;
+ }
+ }
+
+ int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength()
+ + this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength()
+ + this.extraBits;
+
+ int static_len = this.extraBits;
+ ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength);
+ for (int i = 0; i < LiteralNumber; i++)
+ {
+ static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i);
+ }
+
+ ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength);
+ for (int i = 0; i < DistanceNumber; i++)
+ {
+ static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i);
+ }
+
+ if (opt_len >= static_len)
+ {
+ // Force static trees
+ opt_len = static_len;
+ }
+
+ if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3)
+ {
+ // Store Block
+ this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock);
+ }
+ else if (opt_len == static_len)
+ {
+ // Encode with static tree
+ this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3);
+ this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength);
+ this.distTree.SetStaticCodes(StaticDCodes, StaticDLength);
+ this.CompressBlock();
+ this.Reset();
+ }
+ else
+ {
+ // Encode with dynamic tree
+ this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3);
+ this.SendAllTrees(blTreeCodes);
+ this.CompressBlock();
+ this.Reset();
+ }
+ }
+
+ ///
+ /// Get value indicating if internal buffer is full
+ ///
+ /// true if buffer is full
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsFull() => this.lastLiteral >= BufferSize;
+
+ ///
+ /// Add literal to buffer
+ ///
+ /// Literal value to add to buffer.
+ /// Value indicating internal buffer is full
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool TallyLit(int literal)
+ {
+ this.pinnedDistanceBuffer[this.lastLiteral] = 0;
+ this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal;
+ this.literalTree.Frequencies[literal]++;
+ return this.IsFull();
+ }
+
+ ///
+ /// Add distance code and length to literal and distance trees
+ ///
+ /// Distance code
+ /// Length
+ /// Value indicating if internal buffer is full
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool TallyDist(int distance, int length)
+ {
+ this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance;
+ this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3);
+
+ int lc = Lcode(length - 3);
+ this.literalTree.Frequencies[lc]++;
+ if (lc >= 265 && lc < 285)
+ {
+ this.extraBits += (lc - 261) / 4;
+ }
+
+ int dc = Dcode(distance - 1);
+ this.distTree.Frequencies[dc]++;
+ if (dc >= 4)
+ {
+ this.extraBits += (dc >> 1) - 1;
+ }
+
+ return this.IsFull();
+ }
+
+ ///
+ /// Reverse the bits of a 16 bit value.
+ ///
+ /// Value to reverse bits
+ /// Value with bits reversed
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public static short BitReverse(int toReverse)
+ {
+ return (short)(Bit4Reverse[toReverse & 0xF] << 12
+ | Bit4Reverse[(toReverse >> 4) & 0xF] << 8
+ | Bit4Reverse[(toReverse >> 8) & 0xF] << 4
+ | Bit4Reverse[toReverse >> 12]);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.Pending.Dispose();
+ this.distanceBufferHandle.Dispose();
+ this.distanceManagedBuffer.Dispose();
+ this.literalBufferHandle.Dispose();
+ this.literalManagedBuffer.Dispose();
+
+ this.literalTree.Dispose();
+ this.blTree.Dispose();
+ this.distTree.Dispose();
+
+ this.Pending = null;
+ this.literalTree = null;
+ this.blTree = null;
+ this.distTree = null;
+ this.isDisposed = true;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int Lcode(int length)
+ {
+ if (length == 255)
+ {
+ return 285;
+ }
+
+ int code = 257;
+ while (length >= 8)
+ {
+ code += 4;
+ length >>= 1;
+ }
+
+ return code + length;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int Dcode(int distance)
+ {
+ int code = 0;
+ while (distance >= 4)
+ {
+ code += 2;
+ distance >>= 1;
+ }
+
+ return code + distance;
+ }
+
+ private sealed class Tree : IDisposable
+ {
+ private readonly int minNumCodes;
+ private readonly int[] bitLengthCounts;
+ private readonly int maxLength;
+ private bool isDisposed;
+
+ private readonly int elementCount;
+
+ private readonly MemoryAllocator memoryAllocator;
+
+ private IMemoryOwner codesMemoryOwner;
+ private MemoryHandle codesMemoryHandle;
+ private readonly short* codes;
+
+ private IMemoryOwner frequenciesMemoryOwner;
+ private MemoryHandle frequenciesMemoryHandle;
+
+ private IManagedByteBuffer lengthsMemoryOwner;
+ private MemoryHandle lengthsMemoryHandle;
+
+ public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength)
+ {
+ this.memoryAllocator = memoryAllocator;
+ this.elementCount = elements;
+ this.minNumCodes = minCodes;
+ this.maxLength = maxLength;
+
+ this.frequenciesMemoryOwner = memoryAllocator.Allocate(elements);
+ this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin();
+ this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer;
+
+ this.lengthsMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(elements);
+ this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin();
+ this.Length = (byte*)this.lengthsMemoryHandle.Pointer;
+
+ this.codesMemoryOwner = memoryAllocator.Allocate(elements);
+ this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin();
+ this.codes = (short*)this.codesMemoryHandle.Pointer;
+
+ // Maxes out at 15.
+ this.bitLengthCounts = new int[maxLength];
+ }
+
+ public int NumCodes { get; private set; }
+
+ public short* Frequencies { get; }
+
+ public byte* Length { get; }
+
+ ///
+ /// Resets the internal state of the tree
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Reset()
+ {
+ this.frequenciesMemoryOwner.Memory.Span.Clear();
+ this.lengthsMemoryOwner.Memory.Span.Clear();
+ this.codesMemoryOwner.Memory.Span.Clear();
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code)
+ => pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]);
+
+ ///
+ /// Set static codes and length
+ ///
+ /// new codes
+ /// length for new codes
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void SetStaticCodes(ReadOnlySpan staticCodes, ReadOnlySpan staticLengths)
+ {
+ staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span);
+ staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span);
+ }
+
+ ///
+ /// Build dynamic codes and lengths
+ ///
+ public void BuildCodes()
+ {
+ // Maxes out at 15 * 4
+ Span nextCode = stackalloc int[this.maxLength];
+ ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode);
+ ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts);
+
+ int code = 0;
+ for (int bits = 0; bits < this.maxLength; bits++)
+ {
+ Unsafe.Add(ref nextCodeRef, bits) = code;
+ code += Unsafe.Add(ref bitLengthCountsRef, bits) << (15 - bits);
+ }
+
+ for (int i = 0; i < this.NumCodes; i++)
+ {
+ int bits = this.Length[i];
+ if (bits > 0)
+ {
+ this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, bits - 1));
+ Unsafe.Add(ref nextCodeRef, bits - 1) += 1 << (16 - bits);
+ }
+ }
+ }
+
+ public void BuildTree()
+ {
+ int numSymbols = this.elementCount;
+
+ // heap is a priority queue, sorted by frequency, least frequent
+ // nodes first. The heap is a binary tree, with the property, that
+ // the parent node is smaller than both child nodes. This assures
+ // that the smallest node is the first parent.
+ //
+ // The binary tree is encoded in an array: 0 is root node and
+ // the nodes 2*n+1, 2*n+2 are the child nodes of node n.
+ // Maxes out at 286 * 4 so too large for the stack.
+ using (IMemoryOwner heapMemoryOwner = this.memoryAllocator.Allocate(numSymbols))
+ {
+ ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span);
+
+ int heapLen = 0;
+ int maxCode = 0;
+ for (int n = 0; n < numSymbols; n++)
+ {
+ int freq = this.Frequencies[n];
+ if (freq != 0)
+ {
+ // Insert n into heap
+ int pos = heapLen++;
+ int ppos;
+ while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, ppos = (pos - 1) >> 1)] > freq)
+ {
+ Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, ppos);
+ pos = ppos;
+ }
+
+ Unsafe.Add(ref heapRef, pos) = n;
+
+ maxCode = n;
+ }
+ }
+
+ // We could encode a single literal with 0 bits but then we
+ // don't see the literals. Therefore we force at least two
+ // literals to avoid this case. We don't care about order in
+ // this case, both literals get a 1 bit code.
+ while (heapLen < 2)
+ {
+ Unsafe.Add(ref heapRef, heapLen++) = maxCode < 2 ? ++maxCode : 0;
+ }
+
+ this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes);
+
+ int numLeafs = heapLen;
+ int childrenLength = (4 * heapLen) - 2;
+ using (IMemoryOwner childrenMemoryOwner = this.memoryAllocator.Allocate(childrenLength))
+ using (IMemoryOwner valuesMemoryOwner = this.memoryAllocator.Allocate((2 * heapLen) - 1))
+ {
+ ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span);
+ ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span);
+ int numNodes = numLeafs;
+
+ for (int i = 0; i < heapLen; i++)
+ {
+ int node = Unsafe.Add(ref heapRef, i);
+ int i2 = 2 * i;
+ Unsafe.Add(ref childrenRef, i2) = node;
+ Unsafe.Add(ref childrenRef, i2 + 1) = -1;
+ Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8;
+ Unsafe.Add(ref heapRef, i) = i;
+ }
+
+ // Construct the Huffman tree by repeatedly combining the least two
+ // frequent nodes.
+ do
+ {
+ int first = Unsafe.Add(ref heapRef, 0);
+ int last = Unsafe.Add(ref heapRef, --heapLen);
+
+ // Propagate the hole to the leafs of the heap
+ int ppos = 0;
+ int path = 1;
+
+ while (path < heapLen)
+ {
+ if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1)))
+ {
+ path++;
+ }
+
+ Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path);
+ ppos = path;
+ path = (path * 2) + 1;
+ }
+
+ // Now propagate the last element down along path. Normally
+ // it shouldn't go too deep.
+ int lastVal = Unsafe.Add(ref valuesRef, last);
+ while ((path = ppos) > 0
+ && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal)
+ {
+ Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos);
+ }
+
+ Unsafe.Add(ref heapRef, path) = last;
+
+ int second = Unsafe.Add(ref heapRef, 0);
+
+ // Create a new node father of first and second
+ last = numNodes++;
+ Unsafe.Add(ref childrenRef, 2 * last) = first;
+ Unsafe.Add(ref childrenRef, (2 * last) + 1) = second;
+ int mindepth = Math.Min(Unsafe.Add(ref valuesRef, first) & 0xFF, Unsafe.Add(ref valuesRef, second) & 0xFF);
+ Unsafe.Add(ref valuesRef, last) = lastVal = Unsafe.Add(ref valuesRef, first) + Unsafe.Add(ref valuesRef, second) - mindepth + 1;
+
+ // Again, propagate the hole to the leafs
+ ppos = 0;
+ path = 1;
+
+ while (path < heapLen)
+ {
+ if (path + 1 < heapLen
+ && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path)) > Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, path + 1)))
+ {
+ path++;
+ }
+
+ Unsafe.Add(ref heapRef, ppos) = Unsafe.Add(ref heapRef, path);
+ ppos = path;
+ path = (ppos * 2) + 1;
+ }
+
+ // Now propagate the new element down along path
+ while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, Unsafe.Add(ref heapRef, ppos = (path - 1) >> 1)) > lastVal)
+ {
+ Unsafe.Add(ref heapRef, path) = Unsafe.Add(ref heapRef, ppos);
+ }
+
+ Unsafe.Add(ref heapRef, path) = last;
+ }
+ while (heapLen > 1);
+
+ if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1)
+ {
+ DeflateThrowHelper.ThrowHeapViolated();
+ }
+
+ this.BuildLength(childrenMemoryOwner.Memory.Span);
+ }
+ }
+ }
+
+ ///
+ /// Get encoded length
+ ///
+ /// Encoded length, the sum of frequencies * lengths
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public int GetEncodedLength()
+ {
+ int len = 0;
+ for (int i = 0; i < this.elementCount; i++)
+ {
+ len += this.Frequencies[i] * this.Length[i];
+ }
+
+ return len;
+ }
+
+ ///
+ /// Scan a literal or distance tree to determine the frequencies of the codes
+ /// in the bit length tree.
+ ///
+ public void CalcBLFreq(Tree blTree)
+ {
+ int maxCount; // max repeat count
+ int minCount; // min repeat count
+ int count; // repeat count of the current code
+ int curLen = -1; // length of current code
+
+ int i = 0;
+ while (i < this.NumCodes)
+ {
+ count = 1;
+ int nextlen = this.Length[i];
+ if (nextlen == 0)
+ {
+ maxCount = 138;
+ minCount = 3;
+ }
+ else
+ {
+ maxCount = 6;
+ minCount = 3;
+ if (curLen != nextlen)
+ {
+ blTree.Frequencies[nextlen]++;
+ count = 0;
+ }
+ }
+
+ curLen = nextlen;
+ i++;
+
+ while (i < this.NumCodes && curLen == this.Length[i])
+ {
+ i++;
+ if (++count >= maxCount)
+ {
+ break;
+ }
+ }
+
+ if (count < minCount)
+ {
+ blTree.Frequencies[curLen] += (short)count;
+ }
+ else if (curLen != 0)
+ {
+ blTree.Frequencies[Repeat3To6]++;
+ }
+ else if (count <= 10)
+ {
+ blTree.Frequencies[Repeat3To10]++;
+ }
+ else
+ {
+ blTree.Frequencies[Repeat11To138]++;
+ }
+ }
+ }
+
+ ///
+ /// Write the tree values.
+ ///
+ /// The pending buffer.
+ /// The tree to write.
+ public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree)
+ {
+ int maxCount; // max repeat count
+ int minCount; // min repeat count
+ int count; // repeat count of the current code
+ int curLen = -1; // length of current code
+
+ int i = 0;
+ while (i < this.NumCodes)
+ {
+ count = 1;
+ int nextlen = this.Length[i];
+ if (nextlen == 0)
+ {
+ maxCount = 138;
+ minCount = 3;
+ }
+ else
+ {
+ maxCount = 6;
+ minCount = 3;
+ if (curLen != nextlen)
+ {
+ bitLengthTree.WriteSymbol(pendingBuffer, nextlen);
+ count = 0;
+ }
+ }
+
+ curLen = nextlen;
+ i++;
+
+ while (i < this.NumCodes && curLen == this.Length[i])
+ {
+ i++;
+ if (++count >= maxCount)
+ {
+ break;
+ }
+ }
+
+ if (count < minCount)
+ {
+ while (count-- > 0)
+ {
+ bitLengthTree.WriteSymbol(pendingBuffer, curLen);
+ }
+ }
+ else if (curLen != 0)
+ {
+ bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6);
+ pendingBuffer.WriteBits(count - 3, 2);
+ }
+ else if (count <= 10)
+ {
+ bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10);
+ pendingBuffer.WriteBits(count - 3, 3);
+ }
+ else
+ {
+ bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138);
+ pendingBuffer.WriteBits(count - 11, 7);
+ }
+ }
+ }
+
+ private void BuildLength(ReadOnlySpan children)
+ {
+ byte* lengthPtr = this.Length;
+ ref int childrenRef = ref MemoryMarshal.GetReference(children);
+ ref int bitLengthCountsRef = ref MemoryMarshal.GetReference(this.bitLengthCounts);
+
+ int maxLen = this.maxLength;
+ int numNodes = children.Length >> 1;
+ int numLeafs = (numNodes + 1) >> 1;
+ int overflow = 0;
+
+ Array.Clear(this.bitLengthCounts, 0, maxLen);
+
+ // First calculate optimal bit lengths
+ using (IMemoryOwner lengthsMemoryOwner = this.memoryAllocator.Allocate(numNodes, AllocationOptions.Clean))
+ {
+ ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span);
+
+ for (int i = numNodes - 1; i >= 0; i--)
+ {
+ if (children[(2 * i) + 1] != -1)
+ {
+ int bitLength = Unsafe.Add(ref lengthsRef, i) + 1;
+ if (bitLength > maxLen)
+ {
+ bitLength = maxLen;
+ overflow++;
+ }
+
+ Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, 2 * i)) = Unsafe.Add(ref lengthsRef, Unsafe.Add(ref childrenRef, (2 * i) + 1)) = bitLength;
+ }
+ else
+ {
+ // A leaf node
+ int bitLength = Unsafe.Add(ref lengthsRef, i);
+ Unsafe.Add(ref bitLengthCountsRef, bitLength - 1)++;
+ lengthPtr[Unsafe.Add(ref childrenRef, 2 * i)] = (byte)Unsafe.Add(ref lengthsRef, i);
+ }
+ }
+ }
+
+ if (overflow == 0)
+ {
+ return;
+ }
+
+ int incrBitLen = maxLen - 1;
+ do
+ {
+ // Find the first bit length which could increase:
+ while (Unsafe.Add(ref bitLengthCountsRef, --incrBitLen) == 0)
+ {
+ }
+
+ // Move this node one down and remove a corresponding
+ // number of overflow nodes.
+ do
+ {
+ Unsafe.Add(ref bitLengthCountsRef, incrBitLen)--;
+ Unsafe.Add(ref bitLengthCountsRef, ++incrBitLen)++;
+ overflow -= 1 << (maxLen - 1 - incrBitLen);
+ }
+ while (overflow > 0 && incrBitLen < maxLen - 1);
+ }
+ while (overflow > 0);
+
+ // We may have overshot above. Move some nodes from maxLength to
+ // maxLength-1 in that case.
+ Unsafe.Add(ref bitLengthCountsRef, maxLen - 1) += overflow;
+ Unsafe.Add(ref bitLengthCountsRef, maxLen - 2) -= overflow;
+
+ // Now recompute all bit lengths, scanning in increasing
+ // frequency. It is simpler to reconstruct all lengths instead of
+ // fixing only the wrong ones. This idea is taken from 'ar'
+ // written by Haruhiko Okumura.
+ //
+ // The nodes were inserted with decreasing frequency into the childs
+ // array.
+ int nodeIndex = 2 * numLeafs;
+ for (int bits = maxLen; bits != 0; bits--)
+ {
+ int n = Unsafe.Add(ref bitLengthCountsRef, bits - 1);
+ while (n > 0)
+ {
+ int childIndex = 2 * Unsafe.Add(ref childrenRef, nodeIndex++);
+ if (Unsafe.Add(ref childrenRef, childIndex + 1) == -1)
+ {
+ // We found another leaf
+ lengthPtr[Unsafe.Add(ref childrenRef, childIndex)] = (byte)bits;
+ n--;
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.frequenciesMemoryHandle.Dispose();
+ this.frequenciesMemoryOwner.Dispose();
+
+ this.lengthsMemoryHandle.Dispose();
+ this.lengthsMemoryOwner.Dispose();
+
+ this.codesMemoryHandle.Dispose();
+ this.codesMemoryOwner.Dispose();
+
+ this.frequenciesMemoryOwner = null;
+ this.lengthsMemoryOwner = null;
+ this.codesMemoryOwner = null;
+
+ this.isDisposed = true;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs
new file mode 100644
index 000000000..9eeb12cb0
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// A special stream deflating or compressing the bytes that are
+ /// written to it. It uses a Deflater to perform actual deflating.
+ ///
+ internal sealed class DeflaterOutputStream : Stream
+ {
+ private const int BufferLength = 512;
+ private IManagedByteBuffer memoryOwner;
+ private readonly byte[] buffer;
+ private Deflater deflater;
+ private readonly Stream rawStream;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator to use for buffer allocations.
+ /// The output stream where deflated output is written.
+ /// The compression level.
+ public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel)
+ {
+ this.rawStream = rawStream;
+ this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength);
+ this.buffer = this.memoryOwner.Array;
+ this.deflater = new Deflater(memoryAllocator, compressionLevel);
+ }
+
+ ///
+ public override bool CanRead => false;
+
+ ///
+ public override bool CanSeek => false;
+
+ ///
+ public override bool CanWrite => this.rawStream.CanWrite;
+
+ ///
+ public override long Length => this.rawStream.Length;
+
+ ///
+ public override long Position
+ {
+ get
+ {
+ return this.rawStream.Position;
+ }
+
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+
+ ///
+ public override void SetLength(long value) => throw new NotSupportedException();
+
+ ///
+ public override int ReadByte() => throw new NotSupportedException();
+
+ ///
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ ///
+ public override void Flush()
+ {
+ this.deflater.Flush();
+ this.Deflate(true);
+ this.rawStream.Flush();
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ this.deflater.SetInput(buffer, offset, count);
+ this.Deflate();
+ }
+
+ private void Deflate() => this.Deflate(false);
+
+ private void Deflate(bool flushing)
+ {
+ while (flushing || !this.deflater.IsNeedingInput)
+ {
+ int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength);
+
+ if (deflateCount <= 0)
+ {
+ break;
+ }
+
+ this.rawStream.Write(this.buffer, 0, deflateCount);
+ }
+
+ if (!this.deflater.IsNeedingInput)
+ {
+ DeflateThrowHelper.ThrowNoDeflate();
+ }
+ }
+
+ private void Finish()
+ {
+ this.deflater.Finish();
+ while (!this.deflater.IsFinished)
+ {
+ int len = this.deflater.Deflate(this.buffer, 0, BufferLength);
+ if (len <= 0)
+ {
+ break;
+ }
+
+ this.rawStream.Write(this.buffer, 0, len);
+ }
+
+ if (!this.deflater.IsFinished)
+ {
+ DeflateThrowHelper.ThrowNoDeflate();
+ }
+
+ this.rawStream.Flush();
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!this.isDisposed)
+ {
+ if (disposing)
+ {
+ this.Finish();
+ this.deflater.Dispose();
+ this.memoryOwner.Dispose();
+ }
+
+ this.deflater = null;
+ this.memoryOwner = null;
+ this.isDisposed = true;
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
new file mode 100644
index 000000000..a5f00f03c
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
@@ -0,0 +1,179 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Png.Zlib
+{
+ ///
+ /// Stores pending data for writing data to the Deflater.
+ ///
+ internal sealed unsafe class DeflaterPendingBuffer : IDisposable
+ {
+ private readonly byte[] buffer;
+ private readonly byte* pinnedBuffer;
+ private IManagedByteBuffer bufferMemoryOwner;
+ private MemoryHandle bufferMemoryHandle;
+
+ private int start;
+ private int end;
+ private uint bits;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator to use for buffer allocations.
+ public DeflaterPendingBuffer(MemoryAllocator memoryAllocator)
+ {
+ this.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE);
+ this.buffer = this.bufferMemoryOwner.Array;
+ this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin();
+ this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer;
+ }
+
+ ///
+ /// Gets the number of bits written to the buffer.
+ ///
+ public int BitCount { get; private set; }
+
+ ///
+ /// Gets a value indicating whether indicates the buffer has been flushed.
+ ///
+ public bool IsFlushed => this.end == 0;
+
+ ///
+ /// Clear internal state/buffers.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Reset() => this.start = this.end = this.BitCount = 0;
+
+ ///
+ /// Write a short value to buffer LSB first.
+ ///
+ /// The value to write.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void WriteShort(int value)
+ {
+ byte* pinned = this.pinnedBuffer;
+ pinned[this.end++] = unchecked((byte)value);
+ pinned[this.end++] = unchecked((byte)(value >> 8));
+ }
+
+ ///
+ /// Write a block of data to the internal buffer.
+ ///
+ /// The data to write.
+ /// The offset of first byte to write.
+ /// The number of bytes to write.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void WriteBlock(byte[] block, int offset, int length)
+ {
+ Unsafe.CopyBlockUnaligned(ref this.buffer[this.end], ref block[offset], unchecked((uint)length));
+ this.end += length;
+ }
+
+ ///
+ /// Aligns internal buffer on a byte boundary.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void AlignToByte()
+ {
+ if (this.BitCount > 0)
+ {
+ byte* pinned = this.pinnedBuffer;
+ pinned[this.end++] = unchecked((byte)this.bits);
+ if (this.BitCount > 8)
+ {
+ pinned[this.end++] = unchecked((byte)(this.bits >> 8));
+ }
+ }
+
+ this.bits = 0;
+ this.BitCount = 0;
+ }
+
+ ///
+ /// Write bits to internal buffer
+ ///
+ /// source of bits
+ /// number of bits to write
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void WriteBits(int b, int count)
+ {
+ this.bits |= (uint)(b << this.BitCount);
+ this.BitCount += count;
+ if (this.BitCount >= 16)
+ {
+ byte* pinned = this.pinnedBuffer;
+ pinned[this.end++] = unchecked((byte)this.bits);
+ pinned[this.end++] = unchecked((byte)(this.bits >> 8));
+ this.bits >>= 16;
+ this.BitCount -= 16;
+ }
+ }
+
+ ///
+ /// Write a short value to internal buffer most significant byte first
+ ///
+ /// The value to write
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void WriteShortMSB(int value)
+ {
+ byte* pinned = this.pinnedBuffer;
+ pinned[this.end++] = unchecked((byte)(value >> 8));
+ pinned[this.end++] = unchecked((byte)value);
+ }
+
+ ///
+ /// Flushes the pending buffer into the given output array.
+ /// If the output array is to small, only a partial flush is done.
+ ///
+ /// The output array.
+ /// The offset into output array.
+ /// The maximum number of bytes to store.
+ /// The number of bytes flushed.
+ public int Flush(byte[] output, int offset, int length)
+ {
+ if (this.BitCount >= 8)
+ {
+ this.pinnedBuffer[this.end++] = unchecked((byte)this.bits);
+ this.bits >>= 8;
+ this.BitCount -= 8;
+ }
+
+ if (length > this.end - this.start)
+ {
+ length = this.end - this.start;
+
+ Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length));
+ this.start = 0;
+ this.end = 0;
+ }
+ else
+ {
+ Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length));
+ this.start += length;
+ }
+
+ return length;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.bufferMemoryHandle.Dispose();
+ this.bufferMemoryOwner.Dispose();
+ this.bufferMemoryOwner = null;
+ this.isDisposed = true;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/Zlib/README.md b/src/ImageSharp/Formats/Png/Zlib/README.md
index c297a91d5..59f75d05f 100644
--- a/src/ImageSharp/Formats/Png/Zlib/README.md
+++ b/src/ImageSharp/Formats/Png/Zlib/README.md
@@ -1,2 +1,5 @@
-Adler32.cs and Crc32.cs have been copied from
-https://github.com/ygrenier/SharpZipLib.Portable
+Deflatestream implementation adapted from
+
+https://github.com/icsharpcode/SharpZipLib
+
+LIcensed under MIT
diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs
index 8e0bac938..3c52d306f 100644
--- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs
@@ -1,9 +1,9 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
-using System.IO.Compression;
+using SixLabors.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
@@ -38,14 +38,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
///
/// The stream responsible for compressing the input stream.
///
- private System.IO.Compression.DeflateStream deflateStream;
+ // private DeflateStream deflateStream;
+ private DeflaterOutputStream deflateStream;
///
/// Initializes a new instance of the class.
///
+ /// The memory allocator to use for buffer allocations.
/// The stream to compress.
/// The compression level.
- public ZlibDeflateStream(Stream stream, int compressionLevel)
+ public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, int compressionLevel)
{
this.rawStream = stream;
@@ -60,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
// +---+---+
// |CMF|FLG|
// +---+---+
- int cmf = 0x78;
+ const int Cmf = 0x78;
int flg = 218;
// http://stackoverflow.com/a/2331025/277304
@@ -78,29 +80,17 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
}
// Just in case
- flg -= ((cmf * 256) + flg) % 31;
+ flg -= ((Cmf * 256) + flg) % 31;
if (flg < 0)
{
flg += 31;
}
- this.rawStream.WriteByte((byte)cmf);
+ this.rawStream.WriteByte(Cmf);
this.rawStream.WriteByte((byte)flg);
- // Initialize the deflate Stream.
- CompressionLevel level = CompressionLevel.Optimal;
-
- if (compressionLevel >= 1 && compressionLevel <= 5)
- {
- level = CompressionLevel.Fastest;
- }
- else if (compressionLevel == 0)
- {
- level = CompressionLevel.NoCompression;
- }
-
- this.deflateStream = new System.IO.Compression.DeflateStream(this.rawStream, level, true);
+ this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel);
}
///
@@ -110,41 +100,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
public override bool CanSeek => false;
///
- public override bool CanWrite => true;
+ public override bool CanWrite => this.rawStream.CanWrite;
///
- public override long Length => throw new NotSupportedException();
+ public override long Length => this.rawStream.Length;
///
public override long Position
{
- get => throw new NotSupportedException();
- set => throw new NotSupportedException();
+ get
+ {
+ return this.rawStream.Position;
+ }
+
+ set
+ {
+ throw new NotSupportedException();
+ }
}
///
- public override void Flush()
- {
- this.deflateStream?.Flush();
- }
+ public override void Flush() => this.deflateStream.Flush();
///
- public override int Read(byte[] buffer, int offset, int count)
- {
- throw new NotSupportedException();
- }
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
///
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotSupportedException();
- }
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
///
- public override void SetLength(long value)
- {
- throw new NotSupportedException();
- }
+ public override void SetLength(long value) => throw new NotSupportedException();
///
public override void Write(byte[] buffer, int offset, int count)
@@ -164,17 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
if (disposing)
{
// dispose managed resources
- if (this.deflateStream != null)
- {
- this.deflateStream.Dispose();
- this.deflateStream = null;
- }
- else
- {
- // Hack: empty input?
- this.rawStream.WriteByte(3);
- this.rawStream.WriteByte(0);
- }
+ this.deflateStream.Dispose();
// Add the crc
uint crc = (uint)this.adler32.Value;
@@ -184,11 +159,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.rawStream.WriteByte((byte)(crc & 0xFF));
}
- base.Dispose(disposing);
+ this.deflateStream = null;
- // Call the appropriate methods to clean up
- // unmanaged resources here.
- // Note disposing is done.
+ base.Dispose(disposing);
this.isDisposed = true;
}
}
diff --git a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs
index bd9cfa900..af40c333b 100644
--- a/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs
+++ b/src/ImageSharp/Formats/Tga/TgaImageFormatDetector.cs
@@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
{
if (header.Length >= this.HeaderSize)
{
- // There is no magick bytes in a tga file, so at least the image type
+ // There are no magic bytes in a tga file, so at least the image type
// and the colormap type in the header will be checked for a valid value.
if (header[1] != 0 && header[1] != 1)
{
@@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Formats.Tga
return imageType.IsValid();
}
- return true;
+ return false;
}
}
}
diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs
index 8d0df599e..db6a5e165 100644
--- a/src/ImageSharp/Image.Decode.cs
+++ b/src/ImageSharp/Image.Decode.cs
@@ -1,6 +1,7 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Formats;
@@ -47,19 +48,26 @@ namespace SixLabors.ImageSharp
/// The mime type or null if none found.
private static IImageFormat InternalDetectFormat(Stream stream, Configuration config)
{
- // This is probably a candidate for making into a public API in the future!
- int maxHeaderSize = config.MaxHeaderSize;
- if (maxHeaderSize <= 0)
+ // We take a minimum of the stream length vs the max header size and always check below
+ // to ensure that only formats that headers fit within the given buffer length are tested.
+ int headerSize = (int)Math.Min(config.MaxHeaderSize, stream.Length);
+ if (headerSize <= 0)
{
return null;
}
- using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(maxHeaderSize, AllocationOptions.Clean))
+ using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(headerSize, AllocationOptions.Clean))
{
long startPosition = stream.Position;
- stream.Read(buffer.Array, 0, maxHeaderSize);
+ stream.Read(buffer.Array, 0, headerSize);
stream.Position = startPosition;
- return config.ImageFormatsManager.FormatDetectors.Select(x => x.DetectFormat(buffer.GetSpan())).LastOrDefault(x => x != null);
+
+ // Does the given stream contain enough data to fit in the header for the format
+ // and does that data match the format specification?
+ // Individual formats should still check since they are public.
+ return config.ImageFormatsManager.FormatDetectors
+ .Where(x => x.HeaderSize <= headerSize)
+ .Select(x => x.DetectFormat(buffer.GetSpan())).LastOrDefault(x => x != null);
}
}
@@ -123,10 +131,14 @@ namespace SixLabors.ImageSharp
///
/// The or null if suitable info detector not found.
///
- private static IImageInfo InternalIdentity(Stream stream, Configuration config)
+ private static (IImageInfo info, IImageFormat format) InternalIdentity(Stream stream, Configuration config)
{
- var detector = DiscoverDecoder(stream, config, out IImageFormat _) as IImageInfoDetector;
- return detector?.Identify(config, stream);
+ if (!(DiscoverDecoder(stream, config, out IImageFormat format) is IImageInfoDetector detector))
+ {
+ return (null, null);
+ }
+
+ return (detector?.Identify(config, stream), format);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs
index c4336c9ac..60db45f21 100644
--- a/src/ImageSharp/Image.FromStream.cs
+++ b/src/ImageSharp/Image.FromStream.cs
@@ -16,20 +16,20 @@ namespace SixLabors.ImageSharp
public abstract partial class Image
{
///
- /// By reading the header on the provided stream this calculates the images mime type.
+ /// By reading the header on the provided stream this calculates the images format type.
///
/// The image stream to read the header from.
/// Thrown if the stream is not readable.
- /// The mime type or null if none found.
+ /// The format type or null if none found.
public static IImageFormat DetectFormat(Stream stream) => DetectFormat(Configuration.Default, stream);
///
- /// By reading the header on the provided stream this calculates the images mime type.
+ /// By reading the header on the provided stream this calculates the images format type.
///
/// The configuration.
/// The image stream to read the header from.
/// Thrown if the stream is not readable.
- /// The mime type or null if none found.
+ /// The format type or null if none found.
public static IImageFormat DetectFormat(Configuration config, Stream stream)
=> WithSeekableStream(config, stream, s => InternalDetectFormat(s, config));
@@ -41,26 +41,43 @@ namespace SixLabors.ImageSharp
///
/// The or null if suitable info detector not found.
///
- public static IImageInfo Identify(Stream stream) => Identify(Configuration.Default, stream);
+ public static IImageInfo Identify(Stream stream) => Identify(stream, out IImageFormat _);
+
+ ///
+ /// By reading the header on the provided stream this reads the raw image information.
+ ///
+ /// The image stream to read the header from.
+ /// The format type of the decoded image.
+ /// Thrown if the stream is not readable.
+ ///
+ /// The or null if suitable info detector not found.
+ ///
+ public static IImageInfo Identify(Stream stream, out IImageFormat format) => Identify(Configuration.Default, stream, out format);
///
/// Reads the raw image information from the specified stream without fully decoding it.
///
/// The configuration.
/// The image stream to read the information from.
+ /// The format type of the decoded image.
/// Thrown if the stream is not readable.
///
/// The or null if suitable info detector is not found.
///
- public static IImageInfo Identify(Configuration config, Stream stream)
- => WithSeekableStream(config, stream, s => InternalIdentity(s, config ?? Configuration.Default));
+ public static IImageInfo Identify(Configuration config, Stream stream, out IImageFormat format)
+ {
+ (IImageInfo info, IImageFormat format) data = WithSeekableStream(config, stream, s => InternalIdentity(s, config ?? Configuration.Default));
+
+ format = data.format;
+ return data.info;
+ }
///
/// Decode a new instance of the class from the given stream.
/// The pixel format is selected by the decoder.
///
/// The stream containing image information.
- /// the mime type of the decoded image.
+ /// The format type of the decoded image.
/// Thrown if the stream is not readable.
/// Image cannot be loaded.
/// A new .>
@@ -126,7 +143,7 @@ namespace SixLabors.ImageSharp
/// Create a new instance of the class from the given stream.
///
/// The stream containing image information.
- /// the mime type of the decoded image.
+ /// The format type of the decoded image.
/// Thrown if the stream is not readable.
/// Image cannot be loaded.
/// The pixel format.
@@ -180,7 +197,7 @@ namespace SixLabors.ImageSharp
///
/// The configuration options.
/// The stream containing image information.
- /// the mime type of the decoded image.
+ /// The format type of the decoded image.
/// Thrown if the stream is not readable.
/// Image cannot be loaded.
/// The pixel format.
@@ -215,7 +232,7 @@ namespace SixLabors.ImageSharp
///
/// The configuration options.
/// The stream containing image information.
- /// the mime type of the decoded image.
+ /// The format type of the decoded image.
/// Thrown if the stream is not readable.
/// Image cannot be loaded.
/// A new .
diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs
index 06cfdf560..69dff78c1 100644
--- a/src/ImageSharp/Memory/Buffer2D{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2D{T}.cs
@@ -69,23 +69,6 @@ namespace SixLabors.ImageSharp.Memory
}
}
- ///
- /// Creates a new instance that maps to a target rows interval from the current instance.
- ///
- /// The target vertical offset for the rows interval to retrieve.
- /// The desired number of rows to extract.
- /// The new instance with the requested rows interval.
- public Buffer2D Slice(int y, int h)
- {
- DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
- DebugGuard.MustBeGreaterThan(h, 0, nameof(h));
- DebugGuard.MustBeLessThanOrEqualTo(y + h, this.Height, nameof(h));
-
- Memory slice = this.GetMemory().Slice(y * this.Width, h * this.Width);
- var memory = new MemorySource(slice);
- return new Buffer2D(memory, this.Width, h);
- }
-
///
/// Disposes the instance
///
diff --git a/src/ImageSharp/Processing/BokehBlurExecutionMode.cs b/src/ImageSharp/Processing/BokehBlurExecutionMode.cs
deleted file mode 100644
index bc44dca03..000000000
--- a/src/ImageSharp/Processing/BokehBlurExecutionMode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the Apache License, Version 2.0.
-
-using SixLabors.ImageSharp.Processing.Processors.Convolution;
-
-namespace SixLabors.ImageSharp.Processing
-{
- ///
- /// An that indicates execution options for the .
- ///
- public enum BokehBlurExecutionMode
- {
- ///
- /// Indicates that the maximum performance should be prioritized over memory usage.
- ///
- PreferMaximumPerformance,
-
- ///
- /// Indicates that the memory usage should be prioritized over raw performance.
- ///
- PreferLowMemoryUsage
- }
-}
diff --git a/src/ImageSharp/Processing/Extensions/BokehBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/BokehBlurExtensions.cs
index ef20f940a..2bbdd03b0 100644
--- a/src/ImageSharp/Processing/Extensions/BokehBlurExtensions.cs
+++ b/src/ImageSharp/Processing/Extensions/BokehBlurExtensions.cs
@@ -19,15 +19,6 @@ namespace SixLabors.ImageSharp.Processing
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source)
=> source.ApplyProcessor(new BokehBlurProcessor());
- ///
- /// Applies a bokeh blur to the image.
- ///
- /// The image this method extends.
- /// The execution mode to use when applying the processor.
- /// The to allow chaining of operations.
- public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, BokehBlurExecutionMode executionMode)
- => source.ApplyProcessor(new BokehBlurProcessor(executionMode));
-
///
/// Applies a bokeh blur to the image.
///
@@ -39,18 +30,6 @@ namespace SixLabors.ImageSharp.Processing
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma)
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma));
- ///
- /// Applies a bokeh blur to the image.
- ///
- /// The image this method extends.
- /// The 'radius' value representing the size of the area to sample.
- /// The 'components' value representing the number of kernels to use to approximate the bokeh effect.
- /// The gamma highlight factor to use to emphasize bright spots in the source image
- /// The execution mode to use when applying the processor.
- /// The to allow chaining of operations.
- public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, BokehBlurExecutionMode executionMode)
- => source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma, executionMode));
-
///
/// Applies a bokeh blur to the image.
///
@@ -62,18 +41,6 @@ namespace SixLabors.ImageSharp.Processing
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle)
=> source.ApplyProcessor(new BokehBlurProcessor(), rectangle);
- ///
- /// Applies a bokeh blur to the image.
- ///
- /// The image this method extends.
- ///
- /// The structure that specifies the portion of the image object to alter.
- ///
- /// The execution mode to use when applying the processor.
- /// The to allow chaining of operations.
- public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle, BokehBlurExecutionMode executionMode)
- => source.ApplyProcessor(new BokehBlurProcessor(executionMode), rectangle);
-
///
/// Applies a bokeh blur to the image.
///
@@ -87,20 +54,5 @@ namespace SixLabors.ImageSharp.Processing
/// The to allow chaining of operations.
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, Rectangle rectangle)
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma), rectangle);
-
- ///
- /// Applies a bokeh blur to the image.
- ///
- /// The image this method extends.
- /// The 'radius' value representing the size of the area to sample.
- /// The 'components' value representing the number of kernels to use to approximate the bokeh effect.
- /// The gamma highlight factor to use to emphasize bright spots in the source image
- /// The execution mode to use when applying the processor.
- ///
- /// The structure that specifies the portion of the image object to alter.
- ///
- /// The to allow chaining of operations.
- public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, BokehBlurExecutionMode executionMode, Rectangle rectangle)
- => source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma, executionMode), rectangle);
}
}
diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs
index b7e102deb..1812884b8 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs
@@ -26,27 +26,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
///
public const float DefaultGamma = 3F;
- ///
- /// The default execution mode used by the parameterless constructor.
- ///
- public const BokehBlurExecutionMode DefaultExecutionMode = BokehBlurExecutionMode.PreferLowMemoryUsage;
-
///
/// Initializes a new instance of the class.
///
public BokehBlurProcessor()
- : this(DefaultRadius, DefaultComponents, DefaultGamma, DefaultExecutionMode)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// The execution mode to use when applying the processor.
- ///
- public BokehBlurProcessor(BokehBlurExecutionMode executionMode)
- : this(DefaultRadius, DefaultComponents, DefaultGamma, executionMode)
+ : this(DefaultRadius, DefaultComponents, DefaultGamma)
{
}
@@ -63,33 +47,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The gamma highlight factor to use to further process the image.
///
public BokehBlurProcessor(int radius, int components, float gamma)
- : this(radius, components, gamma, DefaultExecutionMode)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// The 'radius' value representing the size of the area to sample.
- ///
- ///
- /// The number of components to use to approximate the original 2D bokeh blur convolution kernel.
- ///
- ///
- /// The gamma highlight factor to use to further process the image.
- ///
- ///
- /// The execution mode to use when applying the processor.
- ///
- public BokehBlurProcessor(int radius, int components, float gamma, BokehBlurExecutionMode executionMode)
{
Guard.MustBeGreaterThanOrEqualTo(gamma, 1, nameof(gamma));
this.Radius = radius;
this.Components = components;
this.Gamma = gamma;
- this.ExecutionMode = executionMode;
}
///
@@ -107,11 +70,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
///
public float Gamma { get; }
- ///
- /// Gets the execution mode to use when applying the effect.
- ///
- public BokehBlurExecutionMode ExecutionMode { get; }
-
///
public IImageProcessor CreatePixelSpecificProcessor(Image source, Rectangle sourceRectangle)
where TPixel : struct, IPixel
diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
index f8fb3f796..efd18dafb 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
@@ -36,11 +36,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
///
private readonly float gamma;
- ///
- /// The execution mode to use when applying the effect
- ///
- private readonly BokehBlurExecutionMode executionMode;
-
///
/// The maximum size of the kernel in either direction
///
@@ -84,7 +79,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
this.kernelSize = (this.radius * 2) + 1;
this.componentsCount = definition.Components;
this.gamma = definition.Gamma;
- this.executionMode = definition.ExecutionMode;
// Reuse the initialized values from the cache, if possible
var parameters = new BokehBlurParameters(this.radius, this.componentsCount);
@@ -280,30 +274,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
this.ApplyGammaExposure(source.PixelBuffer, this.SourceRectangle, this.Configuration);
// Create a 0-filled buffer to use to store the result of the component convolutions
- using (Buffer2D processing = this.Configuration.MemoryAllocator.Allocate2D(source.Size(), AllocationOptions.Clean))
+ using (Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size(), AllocationOptions.Clean))
{
- if (this.executionMode == BokehBlurExecutionMode.PreferLowMemoryUsage)
- {
- // Memory usage priority: allocate a shared buffer and execute the second convolution in sequential mode
- using (Buffer2D buffer = this.Configuration.MemoryAllocator.Allocate2D(source.Width, source.Height + this.radius))
- using (Buffer2D firstPassBuffer = buffer.Slice(this.radius, source.Height))
- using (Buffer2D secondPassBuffer = buffer.Slice(0, source.Height))
- {
- this.OnFrameApplyCore(source, this.SourceRectangle, this.Configuration, processing, firstPassBuffer, secondPassBuffer);
- }
- }
- else
- {
- // Performance priority: allocate two independent buffers and execute both convolutions in parallel mode
- using (Buffer2D firstPassValues = this.Configuration.MemoryAllocator.Allocate2D(source.Size()))
- using (Buffer2D secondPassBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size()))
- {
- this.OnFrameApplyCore(source, this.SourceRectangle, this.Configuration, processing, firstPassValues, secondPassBuffer);
- }
- }
+ // Perform the 1D convolutions on all the kernel components and accumulate the results
+ this.OnFrameApplyCore(source, this.SourceRectangle, this.Configuration, processingBuffer);
// Apply the inverse gamma exposure pass, and write the final pixel data
- this.ApplyInverseGammaExposure(source.PixelBuffer, processing, this.SourceRectangle, this.Configuration);
+ this.ApplyInverseGammaExposure(source.PixelBuffer, processingBuffer, this.SourceRectangle, this.Configuration);
}
}
@@ -314,29 +291,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The structure that specifies the portion of the image object to draw.
/// The configuration.
/// The buffer with the raw pixel data to use to aggregate the results of each convolution.
- /// The complex buffer to use for the first 1D convolution pass for each kernel.
- /// The complex buffer to use for the second 1D convolution pass for each kernel.
private void OnFrameApplyCore(
ImageFrame source,
Rectangle sourceRectangle,
Configuration configuration,
- Buffer2D processingBuffer,
- Buffer2D firstPassBuffer,
- Buffer2D secondPassBuffer)
+ Buffer2D processingBuffer)
{
- // Perform two 1D convolutions for each component in the current instance
- ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan());
- for (int i = 0; i < this.kernels.Length; i++)
+ using (Buffer2D firstPassBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size()))
{
- // Compute the resulting complex buffer for the current component
- var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
- Complex64[] kernel = Unsafe.Add(ref baseRef, i);
- this.ApplyConvolution(firstPassBuffer, source.PixelBuffer, interest, kernel, configuration);
- this.ApplyConvolution(secondPassBuffer, firstPassBuffer, interest, kernel, configuration);
-
- // Add the results of the convolution with the current kernel
- Vector4 parameters = this.kernelParameters[i];
- this.SumProcessingPartials(processingBuffer, secondPassBuffer, sourceRectangle, configuration, parameters.Z, parameters.W);
+ // Perform two 1D convolutions for each component in the current instance
+ ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan());
+ ref Vector4 paramsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan());
+ for (int i = 0; i < this.kernels.Length; i++)
+ {
+ // Compute the resulting complex buffer for the current component
+ var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
+ Complex64[] kernel = Unsafe.Add(ref baseRef, i);
+ Vector4 parameters = Unsafe.Add(ref paramsRef, i);
+
+ // Compute the two 1D convolutions and accumulate the partial results on the target buffer
+ this.ApplyConvolution(firstPassBuffer, source.PixelBuffer, interest, kernel, configuration);
+ this.ApplyConvolution(processingBuffer, firstPassBuffer, interest, kernel, configuration, parameters.Z, parameters.W);
+ }
}
}
@@ -389,19 +365,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// Applies the process to the specified portion of the specified buffer at the specified location
/// and with the specified size.
///
- /// The target values to use to store the results.
+ /// The target values to use to store the results.
/// The source complex values. Cannot be null.
- ///
- /// The structure that specifies the portion of the image object to draw.
- ///
+ /// The structure that specifies the portion of the image object to draw.
/// The 1D kernel.
/// The
+ /// The weight factor for the real component of the complex pixel values.
+ /// The weight factor for the imaginary component of the complex pixel values.
private void ApplyConvolution(
- Buffer2D targetValues,
+ Buffer2D targetValues,
Buffer2D sourceValues,
Rectangle sourceRectangle,
Complex64[] kernel,
- Configuration configuration)
+ Configuration configuration,
+ float z,
+ float w)
{
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
@@ -413,12 +391,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
int width = workingRectangle.Width;
- if (this.executionMode == BokehBlurExecutionMode.PreferLowMemoryUsage)
- {
- configuration = configuration.Clone();
- configuration.MaxDegreeOfParallelism = 1;
- }
-
ParallelHelper.IterateRows(
workingRectangle,
configuration,
@@ -426,11 +398,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
for (int y = rows.Min; y < rows.Max; y++)
{
- Span targetRowSpan = targetValues.GetRowSpan(y).Slice(startX);
+ Span targetRowSpan = targetValues.GetRowSpan(y).Slice(startX);
for (int x = 0; x < width; x++)
{
- Buffer2DUtils.Convolve4(kernel, sourceValues, targetRowSpan, y, x, startY, maxY, startX, maxX);
+ Buffer2DUtils.Convolve4AndAccumulatePartials(kernel, sourceValues, targetRowSpan, y, x, startY, maxY, startX, maxX, z, w);
}
}
});
@@ -536,53 +508,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
}
});
}
-
- ///
- /// Applies the process to the specified portion of the specified at the specified location
- /// and with the specified size.
- ///
- /// The target instance to use to store the results.
- /// The source complex pixels. Cannot be null.
- ///
- /// The structure that specifies the portion of the image object to draw.
- ///
- /// The
- /// The weight factor for the real component of the complex pixel values.
- /// The weight factor for the imaginary component of the complex pixel values.
- private void SumProcessingPartials(
- Buffer2D targetValues,
- Buffer2D sourceValues,
- Rectangle sourceRectangle,
- Configuration configuration,
- float z,
- float w)
- {
- int startY = sourceRectangle.Y;
- int endY = sourceRectangle.Bottom;
- int startX = sourceRectangle.X;
- int endX = sourceRectangle.Right;
-
- var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
- int width = workingRectangle.Width;
-
- ParallelHelper.IterateRows(
- workingRectangle,
- configuration,
- rows =>
- {
- for (int y = rows.Min; y < rows.Max; y++)
- {
- Span targetRowSpan = targetValues.GetRowSpan(y).Slice(startX);
- Span sourceRowSpan = sourceValues.GetRowSpan(y).Slice(startX);
- ref Vector4 baseTargetRef = ref MemoryMarshal.GetReference(targetRowSpan);
- ref ComplexVector4 baseSourceRef = ref MemoryMarshal.GetReference(sourceRowSpan);
-
- for (int x = 0; x < width; x++)
- {
- Unsafe.Add(ref baseTargetRef, x) += Unsafe.Add(ref baseSourceRef, x).WeightedSum(z, w);
- }
- }
- });
- }
}
}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
index 846127275..52faac0cd 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
// Ensure offsets are normalized for cropping and padding.
ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - this.targetOrigin.Y);
- if (kernel.StartIndex + kernel.Length > this.currentWindow.Max)
+ while (kernel.StartIndex + kernel.Length > this.currentWindow.Max)
{
this.Slide();
}
diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs
index 157dadd2c..7bd1b8044 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/EncodePng.cs
@@ -1,9 +1,10 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Drawing.Imaging;
using System.IO;
using BenchmarkDotNet.Attributes;
+using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests;
using SDImage = System.Drawing.Image;
@@ -56,8 +57,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
using (var memoryStream = new MemoryStream())
{
- this.bmpCore.SaveAsPng(memoryStream);
+ var encoder = new PngEncoder { FilterMethod = PngFilterMethod.None };
+ this.bmpCore.SaveAsPng(memoryStream, encoder);
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
index 62e9acf74..1e2846910 100644
--- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
+++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
@@ -3,9 +3,6 @@
using System.IO;
using SixLabors.ImageSharp.Formats;
-using SixLabors.ImageSharp.Formats.Bmp;
-using SixLabors.ImageSharp.Formats.Gif;
-using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@@ -13,6 +10,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
using System;
+ using System.Linq;
using System.Reflection;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -167,38 +165,49 @@ namespace SixLabors.ImageSharp.Tests
[InlineData(100, 100, "jpg")]
[InlineData(100, 10, "jpg")]
[InlineData(10, 100, "jpg")]
- public void CanIdentifyImageLoadedFromBytes(int width, int height, string format)
+ [InlineData(100, 100, "tga")]
+ [InlineData(100, 10, "tga")]
+ [InlineData(10, 100, "tga")]
+ public void CanIdentifyImageLoadedFromBytes(int width, int height, string extension)
{
using (var image = Image.LoadPixelData(new Rgba32[width * height], width, height))
{
using (var memoryStream = new MemoryStream())
{
- image.Save(memoryStream, GetEncoder(format));
+ IImageFormat format = GetFormat(extension);
+ image.Save(memoryStream, format);
memoryStream.Position = 0;
IImageInfo imageInfo = Image.Identify(memoryStream);
Assert.Equal(imageInfo.Width, width);
Assert.Equal(imageInfo.Height, height);
+ memoryStream.Position = 0;
+
+ imageInfo = Image.Identify(memoryStream, out IImageFormat detectedFormat);
+
+ Assert.Equal(format, detectedFormat);
}
}
}
- private static IImageEncoder GetEncoder(string format)
+ [Fact]
+ public void IdentifyReturnsNullWithInvalidStream()
{
- switch (format)
+ byte[] invalid = new byte[10];
+
+ using (var memoryStream = new MemoryStream(invalid))
{
- case "png":
- return new PngEncoder();
- case "gif":
- return new GifEncoder();
- case "bmp":
- return new BmpEncoder();
- case "jpg":
- return new JpegEncoder();
- default:
- throw new ArgumentOutOfRangeException(nameof(format), format, null);
+ IImageInfo imageInfo = Image.Identify(memoryStream, out IImageFormat format);
+
+ Assert.Null(imageInfo);
+ Assert.Null(format);
}
}
+
+ private static IImageFormat GetFormat(string format)
+ {
+ return Configuration.Default.ImageFormats.FirstOrDefault(x => x.FileExtensions.Contains(format));
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
index e976d5a76..660d5b724 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
@@ -54,7 +54,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[InlineData((uint)PngChunkType.Header)] // IHDR
[InlineData((uint)PngChunkType.Palette)] // PLTE
// [InlineData(PngChunkTypes.Data)] //TODO: Figure out how to test this
- [InlineData((uint)PngChunkType.End)] // IEND
public void Decode_IncorrectCRCForCriticalChunk_ExceptionIsThrown(uint chunkType)
{
string chunkName = GetChunkTypeName(chunkType);
@@ -74,26 +73,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
- [Theory]
- [InlineData((uint)PngChunkType.Gamma)] // gAMA
- [InlineData((uint)PngChunkType.Transparency)] // tRNS
- [InlineData((uint)PngChunkType.Physical)] // pHYs: It's ok to test physical as we don't throw for duplicate chunks.
- //[InlineData(PngChunkTypes.Text)] //TODO: Figure out how to test this
- public void Decode_IncorrectCRCForNonCriticalChunk_ExceptionIsThrown(uint chunkType)
- {
- string chunkName = GetChunkTypeName(chunkType);
-
- using (var memStream = new MemoryStream())
- {
- WriteHeaderChunk(memStream);
- WriteChunk(memStream, chunkName);
- WriteDataChunk(memStream);
-
- var decoder = new PngDecoder();
- decoder.Decode(null, memStream);
- }
- }
-
private static string GetChunkTypeName(uint value)
{
var data = new byte[4];
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index e064c0fb0..bdd84038e 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -4,11 +4,11 @@
// ReSharper disable InconsistentNaming
using System.IO;
-
+using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
-
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
@@ -42,6 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
TestImages.Png.Bad.ZlibOverflow,
TestImages.Png.Bad.ZlibOverflow2,
TestImages.Png.Bad.ZlibZtxtBadHeader,
+ TestImages.Png.Bad.Issue1047_BadEndChunk
};
public static readonly string[] TestImages48Bpp =
@@ -90,7 +91,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (Image image = provider.GetImage(new PngDecoder()))
{
image.DebugSave(provider);
- image.CompareToOriginal(provider, ImageComparer.Exact);
+
+ // We don't have another x-plat reference decoder that can be compared for this image.
+ if (provider.Utility.SourceFileOrDescription == TestImages.Png.Bad.Issue1047_BadEndChunk)
+ {
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider, ImageComparer.Exact, (IImageDecoder)SystemDrawingReferenceDecoder.Instance);
+ }
+ }
+ else
+ {
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 2584391bb..8a0cdbfba 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
///
public static readonly TheoryData CompressionLevels = new TheoryData
{
- 1, 2, 3, 4, 5, 6, 7, 8, 9
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
};
public static readonly TheoryData PaletteSizes = new TheoryData
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
index 43384aee7..6171c3c69 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
@@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
destSize.Height,
image0.Height,
Configuration.Default.MemoryAllocator);
- int minimumWorkerAllocationInBytes = verticalKernelMap.MaxDiameter * 2 * destSize.Width * SizeOfVector4;
+ int minimumWorkerAllocationInBytes = verticalKernelMap.MaxDiameter * 2 * destSize.Width * SizeOfVector4;
verticalKernelMap.Dispose();
using (Image image = image0.Clone(configuration))
@@ -419,9 +419,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image image = provider.GetImage())
{
var options = new ResizeOptions
- {
- Size = new Size(image.Width + 200, image.Height + 200), Mode = ResizeMode.BoxPad
- };
+ {
+ Size = new Size(image.Width + 200, image.Height + 200),
+ Mode = ResizeMode.BoxPad
+ };
image.Mutate(x => x.Resize(options));
@@ -462,6 +463,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
}
+ [Theory]
+ [WithFile(TestImages.Jpeg.Issues.IncorrectResize1006, DefaultPixelType)]
+ public void CanResizeLargeImageWithCropMode(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ var options = new ResizeOptions
+ {
+ Size = new Size(480, 600),
+ Mode = ResizeMode.Crop
+ };
+
+ image.Mutate(x => x.Resize(options));
+
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(ValidatorComparer, provider);
+ }
+ }
+
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithMaxMode(TestImageProvider provider)
@@ -486,12 +507,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image image = provider.GetImage())
{
var options = new ResizeOptions
- {
- Size = new Size(
+ {
+ Size = new Size(
(int)Math.Round(image.Width * .75F),
(int)Math.Round(image.Height * .95F)),
- Mode = ResizeMode.Min
- };
+ Mode = ResizeMode.Min
+ };
image.Mutate(x => x.Resize(options));
@@ -508,9 +529,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image image = provider.GetImage())
{
var options = new ResizeOptions
- {
- Size = new Size(image.Width + 200, image.Height), Mode = ResizeMode.Pad
- };
+ {
+ Size = new Size(image.Width + 200, image.Height),
+ Mode = ResizeMode.Pad
+ };
image.Mutate(x => x.Resize(options));
@@ -527,9 +549,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image image = provider.GetImage())
{
var options = new ResizeOptions
- {
- Size = new Size(image.Width / 2, image.Height), Mode = ResizeMode.Stretch
- };
+ {
+ Size = new Size(image.Width / 2, image.Height),
+ Mode = ResizeMode.Stretch
+ };
image.Mutate(x => x.Resize(options));
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 177749869..8f1eca482 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -99,6 +99,7 @@ namespace SixLabors.ImageSharp.Tests
public const string ZlibOverflow = "Png/zlib-overflow.png";
public const string ZlibOverflow2 = "Png/zlib-overflow2.png";
public const string ZlibZtxtBadHeader = "Png/zlib-ztxt-bad-header.png";
+ public const string Issue1047_BadEndChunk = "Png/issues/Issue_1047.png";
}
public static readonly string[] All =
@@ -189,6 +190,7 @@ namespace SixLabors.ImageSharp.Tests
public const string ExifGetString750Load = "Jpg/issues/issue750-exif-load.jpg";
public const string IncorrectQuality845 = "Jpg/issues/Issue845-Incorrect-Quality99.jpg";
public const string IncorrectColorspace855 = "Jpg/issues/issue855-incorrect-colorspace.jpg";
+ public const string IncorrectResize1006 = "Jpg/issues/issue1006-incorrect-resize.jpg";
public static class Fuzz
{
diff --git a/tests/Images/External b/tests/Images/External
index ca4cf8318..d7c099ceb 160000
--- a/tests/Images/External
+++ b/tests/Images/External
@@ -1 +1 @@
-Subproject commit ca4cf8318fe4d09f0fc825686dcd477ebfb5e3e5
+Subproject commit d7c099cebd58f1d3ff997383351d52d28a29df3d
diff --git a/tests/Images/Input/Jpg/issues/issue1006-incorrect-resize.jpg b/tests/Images/Input/Jpg/issues/issue1006-incorrect-resize.jpg
new file mode 100644
index 000000000..3880b869e
--- /dev/null
+++ b/tests/Images/Input/Jpg/issues/issue1006-incorrect-resize.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:90079722b2763f64ff7a47889a7775c9b63ed92239aeff4df437bd1b5a5ab540
+size 618142
diff --git a/tests/Images/Input/Png/issues/Issue_1047.png b/tests/Images/Input/Png/issues/Issue_1047.png
new file mode 100644
index 000000000..7d5a53a9e
--- /dev/null
+++ b/tests/Images/Input/Png/issues/Issue_1047.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4768d4bc3a4aaddb8e3e5cbff2beb706abacfd5448d658564f001811dafd320a
+size 44638