diff --git a/.gitignore b/.gitignore
index 475d6e76b..769a40c6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -221,4 +221,5 @@ artifacts/
# Tests
**/Images/ActualOutput
**/Images/ReferenceOutput
+**/Images/Input/MemoryStress
.DS_Store
diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
index 47a0e0bbf..1193eccee 100644
--- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs
+++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
@@ -4,7 +4,6 @@
using System;
using System.Buffers;
using System.IO;
-using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp
{
diff --git a/src/ImageSharp/Common/Helpers/DebugGuard.cs b/src/ImageSharp/Common/Helpers/DebugGuard.cs
index 9ef7c01c6..f56cb37a8 100644
--- a/src/ImageSharp/Common/Helpers/DebugGuard.cs
+++ b/src/ImageSharp/Common/Helpers/DebugGuard.cs
@@ -37,7 +37,7 @@ namespace SixLabors
/// has a different size than
///
[Conditional("DEBUG")]
- public static void MustBeSameSized(Span target, Span other, string parameterName)
+ public static void MustBeSameSized(ReadOnlySpan target, ReadOnlySpan other, string parameterName)
where T : struct
{
if (target.Length != other.Length)
@@ -57,7 +57,7 @@ namespace SixLabors
/// has less items than
///
[Conditional("DEBUG")]
- public static void MustBeSizedAtLeast(Span target, Span minSpan, string parameterName)
+ public static void MustBeSizedAtLeast(ReadOnlySpan target, ReadOnlySpan minSpan, string parameterName)
where T : struct
{
if (target.Length < minSpan.Length)
diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs
index db65b84cc..ba5c588ca 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/src/ImageSharp/Common/Helpers/Numerics.cs
@@ -879,5 +879,13 @@ namespace SixLabors.ImageSharp
(IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here
}
#endif
+
+ ///
+ /// Fast division with ceiling for numbers.
+ ///
+ /// Divident value.
+ /// Divisor value.
+ /// Ceiled division result.
+ public static uint DivideCeil(uint value, uint divisor) => (value + divisor - 1) / divisor;
}
}
diff --git a/src/ImageSharp/Compression/Zlib/Deflater.cs b/src/ImageSharp/Compression/Zlib/Deflater.cs
index 800c96703..7ff8342aa 100644
--- a/src/ImageSharp/Compression/Zlib/Deflater.cs
+++ b/src/ImageSharp/Compression/Zlib/Deflater.cs
@@ -222,7 +222,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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)
+ public int Deflate(Span output, int offset, int length)
{
int origLength = length;
diff --git a/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
index d3cfa7c3d..506b0f2c1 100644
--- a/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
+++ b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
@@ -130,9 +130,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// This array contains the part of the uncompressed stream that
/// is of relevance. The current character is indexed by strstart.
///
- private IManagedByteBuffer windowMemoryOwner;
+ private IMemoryOwner windowMemoryOwner;
private MemoryHandle windowMemoryHandle;
- private readonly byte[] window;
+ private readonly Memory window;
private readonly byte* pinnedWindowPointer;
private int maxChain;
@@ -153,19 +153,19 @@ namespace SixLabors.ImageSharp.Compression.Zlib
// 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.windowMemoryOwner = memoryAllocator.Allocate(2 * DeflaterConstants.WSIZE);
+ this.window = this.windowMemoryOwner.Memory;
+ this.windowMemoryHandle = this.window.Pin();
this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer;
this.headMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.HASH_SIZE);
this.head = this.headMemoryOwner.Memory;
- this.headMemoryHandle = this.headMemoryOwner.Memory.Pin();
+ this.headMemoryHandle = this.head.Pin();
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer;
this.prevMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.WSIZE);
this.prev = this.prevMemoryOwner.Memory;
- this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin();
+ this.prevMemoryHandle = this.prev.Pin();
this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer;
// We start at index 1, to avoid an implementation deficiency, that
@@ -303,7 +303,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
case DeflaterConstants.DEFLATE_STORED:
if (this.strstart > this.blockStart)
{
- this.huffman.FlushStoredBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
@@ -313,7 +313,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
case DeflaterConstants.DEFLATE_FAST:
if (this.strstart > this.blockStart)
{
- this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
@@ -327,7 +327,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
if (this.strstart > this.blockStart)
{
- this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, false);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
@@ -362,7 +362,10 @@ namespace SixLabors.ImageSharp.Compression.Zlib
more = this.inputEnd - this.inputOff;
}
- Buffer.BlockCopy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more);
+ Unsafe.CopyBlockUnaligned(
+ ref this.window.Span[this.strstart + this.lookahead],
+ ref this.inputBuf[this.inputOff],
+ unchecked((uint)more));
this.inputOff += more;
this.lookahead += more;
@@ -426,7 +429,11 @@ namespace SixLabors.ImageSharp.Compression.Zlib
private void SlideWindow()
{
- Unsafe.CopyBlockUnaligned(ref this.window[0], ref this.window[DeflaterConstants.WSIZE], DeflaterConstants.WSIZE);
+ Unsafe.CopyBlockUnaligned(
+ ref this.window.Span[0],
+ ref this.window.Span[DeflaterConstants.WSIZE],
+ DeflaterConstants.WSIZE);
+
this.matchStart -= DeflaterConstants.WSIZE;
this.strstart -= DeflaterConstants.WSIZE;
this.blockStart -= DeflaterConstants.WSIZE;
@@ -663,7 +670,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
lastBlock = false;
}
- this.huffman.FlushStoredBlock(this.window, this.blockStart, storedLength, lastBlock);
+ this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, storedLength, lastBlock);
this.blockStart += storedLength;
return !(lastBlock || storedLength == 0);
}
@@ -683,7 +690,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
if (this.lookahead == 0)
{
// We are flushing everything
- this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish);
this.blockStart = this.strstart;
return false;
}
@@ -743,7 +750,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
if (this.huffman.IsFull())
{
bool lastBlock = finish && (this.lookahead == 0);
- this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, lastBlock);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, lastBlock);
this.blockStart = this.strstart;
return !lastBlock;
}
@@ -771,7 +778,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
this.prevAvailable = false;
// We are flushing everything
- this.huffman.FlushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish);
this.blockStart = this.strstart;
return false;
}
@@ -846,7 +853,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
}
bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable;
- this.huffman.FlushBlock(this.window, this.blockStart, len, lastBlock);
+ this.huffman.FlushBlock(this.window.Span, this.blockStart, len, lastBlock);
this.blockStart += len;
return !lastBlock;
}
diff --git a/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
index d6892dfd2..27a8d5671 100644
--- a/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
+++ b/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
@@ -41,11 +41,11 @@ namespace SixLabors.ImageSharp.Compression.Zlib
private Tree blTree;
// Buffer for distances
- private readonly IMemoryOwner distanceManagedBuffer;
+ private readonly IMemoryOwner distanceMemoryOwner;
private readonly short* pinnedDistanceBuffer;
private MemoryHandle distanceBufferHandle;
- private readonly IMemoryOwner literalManagedBuffer;
+ private readonly IMemoryOwner literalMemoryOwner;
private readonly short* pinnedLiteralBuffer;
private MemoryHandle literalBufferHandle;
@@ -65,12 +65,12 @@ namespace SixLabors.ImageSharp.Compression.Zlib
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.distanceMemoryOwner = memoryAllocator.Allocate(BufferSize);
+ this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin();
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer;
- this.literalManagedBuffer = memoryAllocator.Allocate(BufferSize);
- this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin();
+ this.literalMemoryOwner = memoryAllocator.Allocate(BufferSize);
+ this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin();
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
@@ -239,7 +239,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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)
+ public void FlushStoredBlock(ReadOnlySpan stored, int storedOffset, int storedLength, bool lastBlock)
{
this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3);
this.Pending.AlignToByte();
@@ -256,7 +256,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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)
+ public void FlushBlock(ReadOnlySpan stored, int storedOffset, int storedLength, bool lastBlock)
{
this.literalTree.Frequencies[EofSymbol]++;
@@ -286,13 +286,13 @@ namespace SixLabors.ImageSharp.Compression.Zlib
+ this.extraBits;
int static_len = this.extraBits;
- ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength);
+ 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);
+ 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);
@@ -419,9 +419,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
{
this.Pending.Dispose();
this.distanceBufferHandle.Dispose();
- this.distanceManagedBuffer.Dispose();
+ this.distanceMemoryOwner.Dispose();
this.literalBufferHandle.Dispose();
- this.literalManagedBuffer.Dispose();
+ this.literalMemoryOwner.Dispose();
this.literalTree.Dispose();
this.blTree.Dispose();
@@ -484,7 +484,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
private IMemoryOwner frequenciesMemoryOwner;
private MemoryHandle frequenciesMemoryHandle;
- private IManagedByteBuffer lengthsMemoryOwner;
+ private IMemoryOwner lengthsMemoryOwner;
private MemoryHandle lengthsMemoryHandle;
public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength)
@@ -498,7 +498,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin();
this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer;
- this.lengthsMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(elements);
+ this.lengthsMemoryOwner = memoryAllocator.Allocate(elements);
this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin();
this.Length = (byte*)this.lengthsMemoryHandle.Pointer;
diff --git a/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
index cbbf7ea79..d949ddf38 100644
--- a/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
+++ b/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.IO;
using SixLabors.ImageSharp.Memory;
@@ -14,8 +15,8 @@ namespace SixLabors.ImageSharp.Compression.Zlib
internal sealed class DeflaterOutputStream : Stream
{
private const int BufferLength = 512;
- private IManagedByteBuffer memoryOwner;
- private readonly byte[] buffer;
+ private IMemoryOwner memoryOwner;
+ private readonly Memory buffer;
private Deflater deflater;
private readonly Stream rawStream;
private bool isDisposed;
@@ -29,8 +30,8 @@ namespace SixLabors.ImageSharp.Compression.Zlib
public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel)
{
this.rawStream = rawStream;
- this.memoryOwner = memoryAllocator.AllocateManagedByteBuffer(BufferLength);
- this.buffer = this.memoryOwner.Array;
+ this.memoryOwner = memoryAllocator.Allocate(BufferLength);
+ this.buffer = this.memoryOwner.Memory;
this.deflater = new Deflater(memoryAllocator, compressionLevel);
}
@@ -49,15 +50,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
///
public override long Position
{
- get
- {
- return this.rawStream.Position;
- }
+ get => this.rawStream.Position;
- set
- {
- throw new NotSupportedException();
- }
+ set => throw new NotSupportedException();
}
///
@@ -93,14 +88,14 @@ namespace SixLabors.ImageSharp.Compression.Zlib
{
while (flushing || !this.deflater.IsNeedingInput)
{
- int deflateCount = this.deflater.Deflate(this.buffer, 0, BufferLength);
+ int deflateCount = this.deflater.Deflate(this.buffer.Span, 0, BufferLength);
if (deflateCount <= 0)
{
break;
}
- this.rawStream.Write(this.buffer, 0, deflateCount);
+ this.rawStream.Write(this.buffer.Span.Slice(0, deflateCount));
}
if (!this.deflater.IsNeedingInput)
@@ -114,13 +109,13 @@ namespace SixLabors.ImageSharp.Compression.Zlib
this.deflater.Finish();
while (!this.deflater.IsFinished)
{
- int len = this.deflater.Deflate(this.buffer, 0, BufferLength);
+ int len = this.deflater.Deflate(this.buffer.Span, 0, BufferLength);
if (len <= 0)
{
break;
}
- this.rawStream.Write(this.buffer, 0, len);
+ this.rawStream.Write(this.buffer.Span.Slice(0, len));
}
if (!this.deflater.IsFinished)
diff --git a/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
index 36dfd92da..8f2c8d398 100644
--- a/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
+++ b/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
@@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib
@@ -13,9 +14,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
///
internal sealed unsafe class DeflaterPendingBuffer : IDisposable
{
- private readonly byte[] buffer;
+ private readonly Memory buffer;
private readonly byte* pinnedBuffer;
- private IManagedByteBuffer bufferMemoryOwner;
+ private IMemoryOwner bufferMemoryOwner;
private MemoryHandle bufferMemoryHandle;
private int start;
@@ -29,9 +30,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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.bufferMemoryOwner = memoryAllocator.Allocate(DeflaterConstants.PENDING_BUF_SIZE);
+ this.buffer = this.bufferMemoryOwner.Memory;
+ this.bufferMemoryHandle = this.buffer.Pin();
this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer;
}
@@ -70,9 +71,13 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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)
+ public void WriteBlock(ReadOnlySpan block, int offset, int length)
{
- Unsafe.CopyBlockUnaligned(ref this.buffer[this.end], ref block[offset], unchecked((uint)length));
+ Unsafe.CopyBlockUnaligned(
+ ref this.buffer.Span[this.end],
+ ref MemoryMarshal.GetReference(block.Slice(offset)),
+ unchecked((uint)length));
+
this.end += length;
}
@@ -136,7 +141,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// 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)
+ public int Flush(Span output, int offset, int length)
{
if (this.BitCount >= 8)
{
@@ -149,13 +154,19 @@ namespace SixLabors.ImageSharp.Compression.Zlib
{
length = this.end - this.start;
- Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length));
+ Unsafe.CopyBlockUnaligned(
+ ref output[offset],
+ ref this.buffer.Span[this.start],
+ unchecked((uint)length));
this.start = 0;
this.end = 0;
}
else
{
- Unsafe.CopyBlockUnaligned(ref output[offset], ref this.buffer[this.start], unchecked((uint)length));
+ Unsafe.CopyBlockUnaligned(
+ ref output[offset],
+ ref this.buffer.Span[this.start],
+ unchecked((uint)length));
this.start += length;
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 7a18d847c..c6ca5b09d 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -348,17 +348,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
where TPixel : unmanaged, IPixel
{
bool isL8 = typeof(TPixel) == typeof(L8);
- using (IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean))
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize8Bit, AllocationOptions.Clean);
+ Span colorPalette = colorPaletteBuffer.GetSpan();
+
+ if (isL8)
{
- Span colorPalette = colorPaletteBuffer.GetSpan();
- if (isL8)
- {
- this.Write8BitGray(stream, image, colorPalette);
- }
- else
- {
- this.Write8BitColor(stream, image, colorPalette);
- }
+ this.Write8BitGray(stream, image, colorPalette);
+ }
+ else
+ {
+ this.Write8BitColor(stream, image, colorPalette);
}
}
@@ -442,7 +441,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
MaxColors = 16
});
using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
- using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean);
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
@@ -486,7 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
MaxColors = 2
});
using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
- using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean);
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index 6424ee23a..70a446512 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -16,29 +16,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal class HuffmanScanDecoder
{
- private readonly JpegFrame frame;
- private readonly HuffmanTable[] dcHuffmanTables;
- private readonly HuffmanTable[] acHuffmanTables;
private readonly BufferedReadStream stream;
- private readonly JpegComponent[] components;
-
- // The restart interval.
- private readonly int restartInterval;
- // The number of interleaved components.
- private readonly int componentsLength;
-
- // The spectral selection start.
- private readonly int spectralStart;
+ ///
+ /// instance containing decoding-related information.
+ ///
+ private JpegFrame frame;
- // The spectral selection end.
- private readonly int spectralEnd;
+ ///
+ /// Shortcut for .Components.
+ ///
+ private JpegComponent[] components;
- // The successive approximation high bit end.
- private readonly int successiveHigh;
+ ///
+ /// Number of component in the current scan.
+ ///
+ private int componentsCount;
- // The successive approximation low bit end.
- private readonly int successiveLow;
+ ///
+ /// The reset interval determined by RST markers.
+ ///
+ private int restartInterval;
// How many mcu's are left to do.
private int todo;
@@ -46,64 +44,85 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero.
private int eobrun;
+ ///
+ /// The DC Huffman tables.
+ ///
+ private readonly HuffmanTable[] dcHuffmanTables;
+
+ ///
+ /// The AC Huffman tables
+ ///
+ private readonly HuffmanTable[] acHuffmanTables;
+
// The unzig data.
private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer;
+ private readonly SpectralConverter spectralConverter;
+
private CancellationToken cancellationToken;
///
/// Initializes a new instance of the class.
///
/// The input stream.
- /// The image frame.
- /// The DC Huffman tables.
- /// The AC Huffman tables.
- /// The length of the components. Different to the array length.
- /// The reset interval.
- /// The spectral selection start.
- /// The spectral selection end.
- /// The successive approximation bit high end.
- /// The successive approximation bit low end.
+ /// Spectral to pixel converter.
/// The token to monitor cancellation.
public HuffmanScanDecoder(
BufferedReadStream stream,
- JpegFrame frame,
- HuffmanTable[] dcHuffmanTables,
- HuffmanTable[] acHuffmanTables,
- int componentsLength,
- int restartInterval,
- int spectralStart,
- int spectralEnd,
- int successiveHigh,
- int successiveLow,
+ SpectralConverter converter,
CancellationToken cancellationToken)
{
this.dctZigZag = ZigZag.CreateUnzigTable();
this.stream = stream;
- this.scanBuffer = new HuffmanScanBuffer(stream);
- this.frame = frame;
- this.dcHuffmanTables = dcHuffmanTables;
- this.acHuffmanTables = acHuffmanTables;
- this.components = frame.Components;
- this.componentsLength = componentsLength;
- this.restartInterval = restartInterval;
- this.todo = restartInterval;
- this.spectralStart = spectralStart;
- this.spectralEnd = spectralEnd;
- this.successiveHigh = successiveHigh;
- this.successiveLow = successiveLow;
+ this.spectralConverter = converter;
this.cancellationToken = cancellationToken;
+
+ // TODO: this is actually a variable value depending on component count
+ const int maxTables = 4;
+ this.dcHuffmanTables = new HuffmanTable[maxTables];
+ this.acHuffmanTables = new HuffmanTable[maxTables];
+ }
+
+ ///
+ /// Sets reset interval determined by RST markers.
+ ///
+ public int ResetInterval
+ {
+ set
+ {
+ this.restartInterval = value;
+ this.todo = value;
+ }
}
+ // The spectral selection start.
+ public int SpectralStart { get; set; }
+
+ // The spectral selection end.
+ public int SpectralEnd { get; set; }
+
+ // The successive approximation high bit end.
+ public int SuccessiveHigh { get; set; }
+
+ // The successive approximation low bit end.
+ public int SuccessiveLow { get; set; }
+
///
/// Decodes the entropy coded data.
///
- public void ParseEntropyCodedData()
+ public void ParseEntropyCodedData(int componentCount)
{
this.cancellationToken.ThrowIfCancellationRequested();
+ this.componentsCount = componentCount;
+
+ this.scanBuffer = new HuffmanScanBuffer(this.stream);
+
+ bool fullScan = this.frame.Progressive || this.frame.MultiScan;
+ this.frame.AllocateComponents(fullScan);
+
if (!this.frame.Progressive)
{
this.ParseBaselineData();
@@ -119,15 +138,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
+ public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
+ {
+ this.frame = frame;
+ this.components = frame.Components;
+
+ this.spectralConverter.InjectFrameData(frame, jpegData);
+ }
+
private void ParseBaselineData()
{
- if (this.componentsLength == 1)
+ if (this.componentsCount == this.frame.ComponentCount)
{
- this.ParseBaselineDataNonInterleaved();
+ this.ParseBaselineDataInterleaved();
}
else
{
- this.ParseBaselineDataInterleaved();
+ this.ParseBaselineDataNonInterleaved();
}
}
@@ -140,7 +167,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
// Pre-derive the huffman table to avoid in-loop checks.
- for (int i = 0; i < this.componentsLength; i++)
+ for (int i = 0; i < this.componentsCount; i++)
{
int order = this.frame.ComponentOrder[i];
JpegComponent component = this.components[order];
@@ -155,12 +182,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
this.cancellationToken.ThrowIfCancellationRequested();
+ // decode from binary to spectral
for (int i = 0; i < mcusPerLine; i++)
{
// Scan an interleaved mcu... process components in order
- int mcuRow = mcu / mcusPerLine;
int mcuCol = mcu % mcusPerLine;
- for (int k = 0; k < this.componentsLength; k++)
+ for (int k = 0; k < this.componentsCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
@@ -175,14 +202,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// by the basic H and V specified for the component
for (int y = 0; y < v; y++)
{
- int blockRow = (mcuRow * v) + y;
- Span blockSpan = component.SpectralBlocks.GetRowSpan(blockRow);
+ Span blockSpan = component.SpectralBlocks.GetRowSpan(y);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int x = 0; x < h; x++)
{
if (buffer.NoData)
{
+ // It is very likely that some spectral data was decoded before we encountered EOI marker
+ // so we need to decode what's left and return (or maybe throw?)
+ this.spectralConverter.ConvertStrideBaseline();
return;
}
@@ -202,6 +231,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
mcu++;
this.HandleRestart();
}
+
+ // convert from spectral to actual pixels via given converter
+ this.spectralConverter.ConvertStrideBaseline();
}
}
@@ -248,9 +280,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// Logic has been adapted from libjpeg.
// See Table B.3 – Scan header parameter size and values. itu-t81.pdf
bool invalid = false;
- if (this.spectralStart == 0)
+ if (this.SpectralStart == 0)
{
- if (this.spectralEnd != 0)
+ if (this.SpectralEnd != 0)
{
invalid = true;
}
@@ -258,22 +290,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
else
{
// Need not check Ss/Se < 0 since they came from unsigned bytes.
- if (this.spectralEnd < this.spectralStart || this.spectralEnd > 63)
+ if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63)
{
invalid = true;
}
// AC scans may have only one component.
- if (this.componentsLength != 1)
+ if (this.componentsCount != 1)
{
invalid = true;
}
}
- if (this.successiveHigh != 0)
+ if (this.SuccessiveHigh != 0)
{
// Successive approximation refinement scan: must have Al = Ah-1.
- if (this.successiveHigh - 1 != this.successiveLow)
+ if (this.SuccessiveHigh - 1 != this.SuccessiveLow)
{
invalid = true;
}
@@ -281,14 +313,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// TODO: How does this affect 12bit jpegs.
// According to libjpeg the range covers 8bit only?
- if (this.successiveLow > 13)
+ if (this.SuccessiveLow > 13)
{
invalid = true;
}
if (invalid)
{
- JpegThrowHelper.ThrowBadProgressiveScan(this.spectralStart, this.spectralEnd, this.successiveHigh, this.successiveLow);
+ JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow);
}
}
@@ -296,7 +328,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
this.CheckProgressiveData();
- if (this.componentsLength == 1)
+ if (this.componentsCount == 1)
{
this.ParseProgressiveDataNonInterleaved();
}
@@ -315,7 +347,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
// Pre-derive the huffman table to avoid in-loop checks.
- for (int k = 0; k < this.componentsLength; k++)
+ for (int k = 0; k < this.componentsCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
@@ -330,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// Scan an interleaved mcu... process components in order
int mcuRow = mcu / mcusPerLine;
int mcuCol = mcu % mcusPerLine;
- for (int k = 0; k < this.componentsLength; k++)
+ for (int k = 0; k < this.componentsCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
@@ -380,7 +412,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int w = component.WidthInBlocks;
int h = component.HeightInBlocks;
- if (this.spectralStart == 0)
+ if (this.SpectralStart == 0)
{
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
@@ -489,7 +521,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref short blockDataRef = ref Unsafe.As(ref block);
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
- if (this.successiveHigh == 0)
+ if (this.SuccessiveHigh == 0)
{
// First scan for DC coefficient, must be first
int s = buffer.DecodeHuffman(ref dcTable);
@@ -500,20 +532,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
s += component.DcPredictor;
component.DcPredictor = s;
- blockDataRef = (short)(s << this.successiveLow);
+ blockDataRef = (short)(s << this.SuccessiveLow);
}
else
{
// Refinement scan for DC coefficient
buffer.CheckBits();
- blockDataRef |= (short)(buffer.GetBits(1) << this.successiveLow);
+ blockDataRef |= (short)(buffer.GetBits(1) << this.SuccessiveLow);
}
}
private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTable)
{
ref short blockDataRef = ref Unsafe.As(ref block);
- if (this.successiveHigh == 0)
+ if (this.SuccessiveHigh == 0)
{
// MCU decoding for AC initial scan (either spectral selection,
// or first pass of successive approximation).
@@ -525,9 +557,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref ZigZag zigzag = ref this.dctZigZag;
- int start = this.spectralStart;
- int end = this.spectralEnd;
- int low = this.successiveLow;
+ int start = this.SpectralStart;
+ int end = this.SpectralEnd;
+ int low = this.SuccessiveLow;
for (int i = start; i <= end; ++i)
{
@@ -571,11 +603,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// Refinement scan for these AC coefficients
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref ZigZag zigzag = ref this.dctZigZag;
- int start = this.spectralStart;
- int end = this.spectralEnd;
+ int start = this.SpectralStart;
+ int end = this.SpectralEnd;
- int p1 = 1 << this.successiveLow;
- int m1 = (-1) << this.successiveLow;
+ int p1 = 1 << this.SuccessiveLow;
+ int m1 = (-1) << this.SuccessiveLow;
int k = start;
@@ -714,5 +746,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
return false;
}
+
+ ///
+ /// Build the huffman table using code lengths and code values.
+ ///
+ /// Table type.
+ /// Table index.
+ /// Code lengths.
+ /// Code values.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values)
+ {
+ HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
+ tables[index] = new HuffmanTable(codeLengths, values);
+ }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
index b1ac1f78f..391dac784 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
@@ -11,26 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal interface IRawJpegData : IDisposable
{
- ///
- /// Gets the image size in pixels.
- ///
- Size ImageSizeInPixels { get; }
-
- ///
- /// Gets the number of components.
- ///
- int ComponentCount { get; }
-
///
/// Gets the color space
///
JpegColorSpace ColorSpace { get; }
- ///
- /// Gets the number of bits used for precision.
- ///
- int Precision { get; }
-
///
/// Gets the components.
///
@@ -41,4 +26,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
Block8x8F[] QuantizationTables { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs
index e0311dafe..7cfbaddcc 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs
@@ -38,11 +38,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
private Size subSamplingDivisors;
- ///
- /// Defines the maximum value derived from the bitdepth.
- ///
- private readonly int maximumValue;
-
///
/// Initializes a new instance of the struct.
///
@@ -53,7 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int qtIndex = component.QuantizationTableIndex;
this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]);
this.subSamplingDivisors = component.SubSamplingDivisors;
- this.maximumValue = (int)MathF.Pow(2, decoder.Precision) - 1;
this.SourceBlock = default;
this.WorkspaceBlock1 = default;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
index 5c3ee6e28..ba3dfb629 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
@@ -106,31 +106,43 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.SpectralBlocks = null;
}
- public void Init()
+ ///
+ /// Initializes component for future buffers initialization.
+ ///
+ /// Maximal horizontal subsampling factor among all the components.
+ /// Maximal vertical subsampling factor among all the components.
+ public void Init(int maxSubFactorH, int maxSubFactorV)
{
this.WidthInBlocks = (int)MathF.Ceiling(
- MathF.Ceiling(this.Frame.SamplesPerLine / 8F) * this.HorizontalSamplingFactor / this.Frame.MaxHorizontalFactor);
+ MathF.Ceiling(this.Frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / maxSubFactorH);
this.HeightInBlocks = (int)MathF.Ceiling(
- MathF.Ceiling(this.Frame.Scanlines / 8F) * this.VerticalSamplingFactor / this.Frame.MaxVerticalFactor);
+ MathF.Ceiling(this.Frame.PixelHeight / 8F) * this.VerticalSamplingFactor / maxSubFactorV);
int blocksPerLineForMcu = this.Frame.McusPerLine * this.HorizontalSamplingFactor;
int blocksPerColumnForMcu = this.Frame.McusPerColumn * this.VerticalSamplingFactor;
this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu);
- JpegComponent c0 = this.Frame.Components[0];
- this.SubSamplingDivisors = c0.SamplingFactors.DivideBy(this.SamplingFactors);
+ this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors);
if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0)
{
JpegThrowHelper.ThrowBadSampling();
}
+ }
+
+ public void AllocateSpectral(bool fullScan)
+ {
+ if (this.SpectralBlocks != null)
+ {
+ // this method will be called each scan marker so we need to allocate only once
+ return;
+ }
- int totalNumberOfBlocks = blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
- int width = this.WidthInBlocks + 1;
- int height = totalNumberOfBlocks / width;
+ int spectralAllocWidth = this.SizeInBlocks.Width;
+ int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor;
- this.SpectralBlocks = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean);
+ this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean);
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
index fc1ebaf92..9a659d621 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
@@ -2,15 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
///
- /// Encapsulates postprocessing data for one component for .
+ /// Encapsulates spectral data to rgba32 processing for one component.
///
internal class JpegComponentPostProcessor : IDisposable
{
@@ -24,26 +21,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
private readonly Size blockAreaSize;
+ ///
+ /// Jpeg frame instance containing required decoding metadata.
+ ///
+ private readonly JpegFrame frame;
+
///
/// Initializes a new instance of the class.
///
- public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegImagePostProcessor imagePostProcessor, IJpegComponent component)
+ public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
{
+ this.frame = frame;
+
this.Component = component;
- this.ImagePostProcessor = imagePostProcessor;
+ this.RawJpeg = rawJpeg;
this.blockAreaSize = this.Component.SubSamplingDivisors * 8;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned(
- imagePostProcessor.PostProcessorBufferSize.Width,
- imagePostProcessor.PostProcessorBufferSize.Height,
+ postProcessorBufferSize.Width,
+ postProcessorBufferSize.Height,
this.blockAreaSize.Height);
- this.BlockRowsPerStep = JpegImagePostProcessor.BlockRowsPerStep / this.Component.SubSamplingDivisors.Height;
+ this.BlockRowsPerStep = postProcessorBufferSize.Height / 8 / this.Component.SubSamplingDivisors.Height;
}
- ///
- /// Gets the
- ///
- public JpegImagePostProcessor ImagePostProcessor { get; }
+ public IRawJpegData RawJpeg { get; }
///
/// Gets the
@@ -66,26 +67,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public int BlockRowsPerStep { get; }
///
- public void Dispose()
- {
- this.ColorBuffer.Dispose();
- }
+ public void Dispose() => this.ColorBuffer.Dispose();
///
/// Invoke for block rows, copy the result into .
///
- public void CopyBlocksToColorBuffer()
+ public void CopyBlocksToColorBuffer(int step)
{
- var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component);
- float maximumValue = MathF.Pow(2, this.ImagePostProcessor.RawJpeg.Precision) - 1;
+ Buffer2D spectralBuffer = this.Component.SpectralBlocks;
+
+ var blockPp = new JpegBlockPostProcessor(this.RawJpeg, this.Component);
+
+ float maximumValue = this.frame.MaxColorChannelValue;
int destAreaStride = this.ColorBuffer.Width;
+ int yBlockStart = step * this.BlockRowsPerStep;
+
for (int y = 0; y < this.BlockRowsPerStep; y++)
{
- int yBlock = this.currentComponentRowInBlocks + y;
+ int yBlock = yBlockStart + y;
- if (yBlock >= this.SizeInBlocks.Height)
+ if (yBlock >= spectralBuffer.Height)
{
break;
}
@@ -93,10 +96,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int yBuffer = y * this.blockAreaSize.Height;
Span colorBufferRow = this.ColorBuffer.GetRowSpan(yBuffer);
- Span blockRow = this.Component.SpectralBlocks.GetRowSpan(yBlock);
+ Span blockRow = spectralBuffer.GetRowSpan(yBlock);
// see: https://github.com/SixLabors/ImageSharp/issues/824
- int widthInBlocks = Math.Min(this.Component.SpectralBlocks.Width, this.SizeInBlocks.Width);
+ int widthInBlocks = Math.Min(spectralBuffer.Width, this.SizeInBlocks.Width);
for (int xBlock = 0; xBlock < widthInBlocks; xBlock++)
{
@@ -107,7 +110,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
blockPp.ProcessBlockColorsInto(ref block, ref destAreaOrigin, destAreaStride, maximumValue);
}
}
+ }
+
+ public void ClearSpectralBuffers()
+ {
+ Buffer2D spectralBlocks = this.Component.SpectralBlocks;
+ for (int i = 0; i < spectralBlocks.Height; i++)
+ {
+ spectralBlocks.GetRowSpan(i).Clear();
+ }
+ }
+ public void CopyBlocksToColorBuffer()
+ {
+ this.CopyBlocksToColorBuffer(this.currentComponentRowInBlocks);
this.currentComponentRowInBlocks += this.BlockRowsPerStep;
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
index 827afe38d..fc109be26 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
@@ -10,35 +10,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal sealed class JpegFrame : IDisposable
{
+ public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount)
+ {
+ this.Extended = sofMarker.Marker == JpegConstants.Markers.SOF1;
+ this.Progressive = sofMarker.Marker == JpegConstants.Markers.SOF2;
+
+ this.Precision = precision;
+ this.MaxColorChannelValue = MathF.Pow(2, precision) - 1;
+
+ this.PixelWidth = width;
+ this.PixelHeight = height;
+
+ this.ComponentCount = componentCount;
+ }
+
+ ///
+ /// Gets a value indicating whether the frame uses the extended specification.
+ ///
+ public bool Extended { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the frame uses the progressive specification.
+ ///
+ public bool Progressive { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether the frame is encoded using multiple scans (SOS markers).
+ ///
+ ///
+ /// This is true for progressive and baseline non-interleaved images.
+ ///
+ public bool MultiScan { get; set; }
+
///
- /// Gets or sets a value indicating whether the frame uses the extended specification.
+ /// Gets the precision.
///
- public bool Extended { get; set; }
+ public byte Precision { get; private set; }
///
- /// Gets or sets a value indicating whether the frame uses the progressive specification.
+ /// Gets the maximum color value derived from .
///
- public bool Progressive { get; set; }
+ public float MaxColorChannelValue { get; private set; }
///
- /// Gets or sets the precision.
+ /// Gets the number of pixel per row.
///
- public byte Precision { get; set; }
+ public int PixelHeight { get; private set; }
///
- /// Gets or sets the number of scanlines within the frame.
+ /// Gets the number of pixels per line.
///
- public int Scanlines { get; set; }
+ public int PixelWidth { get; private set; }
///
- /// Gets or sets the number of samples per scanline.
+ /// Gets the pixel size of the image.
///
- public int SamplesPerLine { get; set; }
+ public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight);
///
- /// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4.
+ /// Gets the number of components within a frame.
///
- public byte ComponentCount { get; set; }
+ public byte ComponentCount { get; private set; }
///
/// Gets or sets the component id collection.
@@ -57,24 +89,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public JpegComponent[] Components { get; set; }
///
- /// Gets or sets the maximum horizontal sampling factor.
+ /// Gets or sets the number of MCU's per line.
///
- public int MaxHorizontalFactor { get; set; }
+ public int McusPerLine { get; set; }
///
- /// Gets or sets the maximum vertical sampling factor.
+ /// Gets or sets the number of MCU's per column.
///
- public int MaxVerticalFactor { get; set; }
+ public int McusPerColumn { get; set; }
///
- /// Gets or sets the number of MCU's per line.
+ /// Gets the mcu size of the image.
///
- public int McusPerLine { get; set; }
+ public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn);
///
- /// Gets or sets the number of MCU's per column.
+ /// Gets the color depth, in number of bits per pixel.
///
- public int McusPerColumn { get; set; }
+ public int BitsPerPixel => this.ComponentCount * this.Precision;
///
public void Dispose()
@@ -93,15 +125,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
/// Allocates the frame component blocks.
///
- public void InitComponents()
+ /// Maximal horizontal subsampling factor among all the components.
+ /// Maximal vertical subsampling factor among all the components.
+ public void Init(int maxSubFactorH, int maxSubFactorV)
{
- this.McusPerLine = (int)MathF.Ceiling(this.SamplesPerLine / 8F / this.MaxHorizontalFactor);
- this.McusPerColumn = (int)MathF.Ceiling(this.Scanlines / 8F / this.MaxVerticalFactor);
+ this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8);
+ this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8);
for (int i = 0; i < this.ComponentCount; i++)
{
JpegComponent component = this.Components[i];
- component.Init();
+ component.Init(maxSubFactorH, maxSubFactorV);
+ }
+ }
+
+ public void AllocateComponents(bool fullScan)
+ {
+ for (int i = 0; i < this.ComponentCount; i++)
+ {
+ JpegComponent component = this.Components[i];
+ component.AllocateSpectral(fullScan);
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs
deleted file mode 100644
index 5b0331c85..000000000
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.Buffers;
-using System.Numerics;
-using System.Threading;
-using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Memory;
-using SixLabors.ImageSharp.PixelFormats;
-using JpegColorConverter = SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters.JpegColorConverter;
-
-namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
-{
- ///
- /// Encapsulates the execution od post-processing algorithms to be applied on a to produce a valid :
- /// (1) Dequantization
- /// (2) IDCT
- /// (3) Color conversion form one of the -s into a buffer of RGBA values
- /// (4) Packing pixels from the buffer.
- /// These operations are executed in steps.
- /// image rows are converted in one step,
- /// which means that size of the allocated memory is limited (does not depend on ).
- ///
- internal class JpegImagePostProcessor : IDisposable
- {
- private readonly Configuration configuration;
-
- ///
- /// The number of block rows to be processed in one Step.
- ///
- public const int BlockRowsPerStep = 4;
-
- ///
- /// The number of image pixel rows to be processed in one step.
- ///
- public const int PixelRowsPerStep = 4 * 8;
-
- ///
- /// Temporal buffer to store a row of colors.
- ///
- private readonly IMemoryOwner rgbaBuffer;
-
- ///
- /// The corresponding to the current determined by .
- ///
- private readonly JpegColorConverter colorConverter;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The to configure internal operations.
- /// The representing the uncompressed spectral Jpeg data
- public JpegImagePostProcessor(Configuration configuration, IRawJpegData rawJpeg)
- {
- this.configuration = configuration;
- this.RawJpeg = rawJpeg;
- IJpegComponent c0 = rawJpeg.Components[0];
- this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep;
- this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep);
-
- MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
-
- this.ComponentProcessors = new JpegComponentPostProcessor[rawJpeg.Components.Length];
- for (int i = 0; i < rawJpeg.Components.Length; i++)
- {
- this.ComponentProcessors[i] = new JpegComponentPostProcessor(memoryAllocator, this, rawJpeg.Components[i]);
- }
-
- this.rgbaBuffer = memoryAllocator.Allocate(rawJpeg.ImageSizeInPixels.Width);
- this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision);
- }
-
- ///
- /// Gets the instances.
- ///
- public JpegComponentPostProcessor[] ComponentProcessors { get; }
-
- ///
- /// Gets the to be processed.
- ///
- public IRawJpegData RawJpeg { get; }
-
- ///
- /// Gets the total number of post processor steps deduced from the height of the image and .
- ///
- public int NumberOfPostProcessorSteps { get; }
-
- ///
- /// Gets the size of the temporary buffers we need to allocate into .
- ///
- public Size PostProcessorBufferSize { get; }
-
- ///
- /// Gets the value of the counter that grows by each step by .
- ///
- public int PixelRowCounter { get; private set; }
-
- ///
- public void Dispose()
- {
- foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors)
- {
- cpp.Dispose();
- }
-
- this.rgbaBuffer.Dispose();
- }
-
- ///
- /// Process all pixels into 'destination'. The image dimensions should match .
- ///
- /// The pixel type
- /// The destination image
- /// The token to request cancellation.
- public void PostProcess(ImageFrame destination, CancellationToken cancellationToken)
- where TPixel : unmanaged, IPixel
- {
- this.PixelRowCounter = 0;
-
- if (this.RawJpeg.ImageSizeInPixels != destination.Size())
- {
- throw new ArgumentException("Input image is not of the size of the processed one!");
- }
-
- while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height)
- {
- cancellationToken.ThrowIfCancellationRequested();
- this.DoPostProcessorStep(destination);
- }
- }
-
- ///
- /// Execute one step processing pixel rows into 'destination'.
- ///
- /// The pixel type
- /// The destination image.
- public void DoPostProcessorStep(ImageFrame destination)
- where TPixel : unmanaged, IPixel
- {
- foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors)
- {
- cpp.CopyBlocksToColorBuffer();
- }
-
- this.ConvertColorsInto(destination);
-
- this.PixelRowCounter += PixelRowsPerStep;
- }
-
- ///
- /// Convert and copy row of colors into 'destination' starting at row .
- ///
- /// The pixel type
- /// The destination image
- private void ConvertColorsInto(ImageFrame destination)
- where TPixel : unmanaged, IPixel
- {
- int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep);
-
- var buffers = new Buffer2D[this.ComponentProcessors.Length];
- for (int i = 0; i < this.ComponentProcessors.Length; i++)
- {
- buffers[i] = this.ComponentProcessors[i].ColorBuffer;
- }
-
- for (int yy = this.PixelRowCounter; yy < maxY; yy++)
- {
- int y = yy - this.PixelRowCounter;
-
- var values = new JpegColorConverter.ComponentValues(buffers, y);
- this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan());
-
- Span destRow = destination.GetPixelRowSpan(yy);
-
- // TODO: Investigate if slicing is actually necessary
- PixelOperations.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow);
- }
- }
- }
-}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
new file mode 100644
index 000000000..e84d13ff1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
+{
+ ///
+ /// Converter used to convert jpeg spectral data.
+ ///
+ ///
+ /// This is tightly coupled with and .
+ ///
+ internal abstract class SpectralConverter
+ {
+ ///
+ /// Injects jpeg image decoding metadata.
+ ///
+ ///
+ /// This is guaranteed to be called only once at SOF marker by .
+ ///
+ /// instance containing decoder-specific parameters.
+ /// instance containing decoder-specific parameters.
+ public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData);
+
+ ///
+ /// Called once per spectral stride for each component in .
+ /// This is called only for baseline interleaved jpegs.
+ ///
+ ///
+ /// Spectral 'stride' doesn't particularly mean 'single stride'.
+ /// Actual stride height depends on the subsampling factor of the given component.
+ ///
+ public abstract void ConvertStrideBaseline();
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
new file mode 100644
index 000000000..50cfa0188
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Numerics;
+using System.Threading;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
+{
+ internal sealed class SpectralConverter : SpectralConverter, IDisposable
+ where TPixel : unmanaged, IPixel
+ {
+ private readonly Configuration configuration;
+
+ private CancellationToken cancellationToken;
+
+ private JpegComponentPostProcessor[] componentProcessors;
+
+ private JpegColorConverter colorConverter;
+
+ private IMemoryOwner rgbaBuffer;
+
+ private Buffer2D pixelBuffer;
+
+ private int blockRowsPerStep;
+
+ private int pixelRowsPerStep;
+
+ private int pixelRowCounter;
+
+ public SpectralConverter(Configuration configuration, CancellationToken cancellationToken)
+ {
+ this.configuration = configuration;
+ this.cancellationToken = cancellationToken;
+ }
+
+ private bool Converted => this.pixelRowCounter >= this.pixelBuffer.Height;
+
+ public Buffer2D PixelBuffer
+ {
+ get
+ {
+ if (!this.Converted)
+ {
+ int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep);
+
+ for (int step = 0; step < steps; step++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+ this.ConvertNextStride(step);
+ }
+ }
+
+ return this.pixelBuffer;
+ }
+ }
+
+ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
+ {
+ MemoryAllocator allocator = this.configuration.MemoryAllocator;
+
+ // iteration data
+ IJpegComponent c0 = frame.Components[0];
+
+ const int blockPixelHeight = 8;
+ this.blockRowsPerStep = c0.SamplingFactors.Height;
+ this.pixelRowsPerStep = this.blockRowsPerStep * blockPixelHeight;
+
+ // pixel buffer for resulting image
+ this.pixelBuffer = allocator.Allocate2D(frame.PixelWidth, frame.PixelHeight, AllocationOptions.Clean);
+
+ // component processors from spectral to Rgba32
+ var postProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, this.pixelRowsPerStep);
+ this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length];
+ for (int i = 0; i < this.componentProcessors.Length; i++)
+ {
+ this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]);
+ }
+
+ // single 'stride' rgba32 buffer for conversion between spectral and TPixel
+ this.rgbaBuffer = allocator.Allocate(frame.PixelWidth);
+
+ // color converter from Rgba32 to TPixel
+ this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision);
+ }
+
+ public override void ConvertStrideBaseline()
+ {
+ // Convert next pixel stride using single spectral `stride'
+ // Note that zero passing eliminates the need of virtual call from JpegComponentPostProcessor
+ this.ConvertNextStride(spectralStep: 0);
+
+ // Clear spectral stride - this is VERY important as jpeg possibly won't fill entire buffer each stride
+ // Which leads to decoding artifacts
+ // Note that this code clears all buffers of the post processors, it's their responsibility to allocate only single stride
+ foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
+ {
+ cpp.ClearSpectralBuffers();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.componentProcessors != null)
+ {
+ foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
+ {
+ cpp.Dispose();
+ }
+ }
+
+ this.rgbaBuffer?.Dispose();
+ }
+
+ private void ConvertNextStride(int spectralStep)
+ {
+ int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep);
+
+ var buffers = new Buffer2D[this.componentProcessors.Length];
+ for (int i = 0; i < this.componentProcessors.Length; i++)
+ {
+ this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep);
+ buffers[i] = this.componentProcessors[i].ColorBuffer;
+ }
+
+ for (int yy = this.pixelRowCounter; yy < maxY; yy++)
+ {
+ int y = yy - this.pixelRowCounter;
+
+ var values = new JpegColorConverter.ComponentValues(buffers, y);
+ this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan());
+
+ Span destRow = this.pixelBuffer.GetRowSpan(yy);
+
+ // TODO: Investigate if slicing is actually necessary
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow);
+ }
+
+ this.pixelRowCounter += this.pixelRowsPerStep;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
index f5ef77091..6d3620c62 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
@@ -13,8 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
public static void LoadAndStretchEdges(RowOctet source, Span dest, Point start, Size sampleSize, Size totalSize)
{
- DebugGuard.MustBeBetweenOrEqualTo(start.X, 1, totalSize.Width - 1, nameof(start.X));
- DebugGuard.MustBeBetweenOrEqualTo(start.Y, 1, totalSize.Height - 1, nameof(start.Y));
+ DebugGuard.MustBeBetweenOrEqualTo(start.X, 0, totalSize.Width - 1, nameof(start.X));
+ DebugGuard.MustBeBetweenOrEqualTo(start.Y, 0, totalSize.Height - 1, nameof(start.Y));
int width = Math.Min(sampleSize.Width, totalSize.Width - start.X);
int height = Math.Min(sampleSize.Height, totalSize.Height - start.Y);
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 8571cf0ec..77b1b44af 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -29,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The only supported precision
///
- private readonly int[] supportedPrecisions = { 8, 12 };
+ private readonly byte[] supportedPrecisions = { 8, 12 };
///
/// The buffer used to temporarily store bytes read from the stream.
@@ -41,21 +42,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
private readonly byte[] markerBuffer = new byte[2];
- ///
- /// The DC Huffman tables.
- ///
- private HuffmanTable[] dcHuffmanTables;
-
- ///
- /// The AC Huffman tables
- ///
- private HuffmanTable[] acHuffmanTables;
-
- ///
- /// The reset interval determined by RST markers.
- ///
- private ushort resetInterval;
-
///
/// Whether the image has an EXIF marker.
///
@@ -96,6 +82,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
private AdobeMarker adobe;
+ ///
+ /// Scan decoder.
+ ///
+ private HuffmanScanDecoder scanDecoder;
+
///
/// Initializes a new instance of the class.
///
@@ -116,30 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public JpegFrame Frame { get; private set; }
///
- public Size ImageSizeInPixels { get; private set; }
-
- ///
- Size IImageDecoderInternals.Dimensions => this.ImageSizeInPixels;
-
- ///
- /// Gets the number of MCU blocks in the image as .
- ///
- public Size ImageSizeInMCU { get; private set; }
-
- ///
- /// Gets the image width
- ///
- public int ImageWidth => this.ImageSizeInPixels.Width;
-
- ///
- /// Gets the image height
- ///
- public int ImageHeight => this.ImageSizeInPixels.Height;
-
- ///
- /// Gets the color depth, in number of bits per pixel.
- ///
- public int BitsPerPixel => this.ComponentCount * this.Frame.Precision;
+ Size IImageDecoderInternals.Dimensions => this.Frame.PixelSize;
///
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
@@ -151,15 +119,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
public ImageMetadata Metadata { get; private set; }
- ///
- public int ComponentCount { get; private set; }
-
///
public JpegColorSpace ColorSpace { get; private set; }
- ///
- public int Precision { get; private set; }
-
///
/// Gets the components.
///
@@ -212,34 +174,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- this.ParseStream(stream, cancellationToken: cancellationToken);
+ using var spectralConverter = new SpectralConverter(this.Configuration, cancellationToken);
+
+ var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
+
+ this.ParseStream(stream, scanDecoder, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitDerivedMetadataProperties();
- return this.PostProcessIntoImage(cancellationToken);
+
+ return new Image(this.Configuration, spectralConverter.PixelBuffer, this.Metadata);
}
///
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
- this.ParseStream(stream, true, cancellationToken);
+ this.ParseStream(stream, scanDecoder: null, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitDerivedMetadataProperties();
- return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata);
+ Size pixelSize = this.Frame.PixelSize;
+ return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
}
///
- /// Parses the input stream for file markers
+ /// Parses the input stream for file markers.
///
- /// The input stream
- /// Whether to decode metadata only.
+ /// The input stream.
+ /// Scan decoder used exclusively to decode SOS marker.
/// The token to monitor cancellation.
- public void ParseStream(BufferedReadStream stream, bool metadataOnly = false, CancellationToken cancellationToken = default)
+ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDecoder, CancellationToken cancellationToken)
{
+ bool metadataOnly = scanDecoder == null;
+
+ this.scanDecoder = scanDecoder;
+
this.Metadata = new ImageMetadata();
// Check for the Start Of Image marker.
@@ -255,14 +227,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
this.QuantizationTables = new Block8x8F[4];
- // Only assign what we need
- if (!metadataOnly)
- {
- const int maxTables = 4;
- this.dcHuffmanTables = new HuffmanTable[maxTables];
- this.acHuffmanTables = new HuffmanTable[maxTables];
- }
-
// Break only when we discover a valid EOI marker.
// https://github.com/SixLabors/ImageSharp/issues/695
while (fileMarker.Marker != JpegConstants.Markers.EOI
@@ -286,7 +250,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS:
if (!metadataOnly)
{
- this.ProcessStartOfScanMarker(stream, cancellationToken);
+ this.ProcessStartOfScanMarker(stream, remaining, cancellationToken);
break;
}
else
@@ -377,22 +341,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Set large fields to null.
this.Frame = null;
- this.dcHuffmanTables = null;
- this.acHuffmanTables = null;
+ this.scanDecoder = null;
}
///
/// Returns the correct colorspace based on the image component count
///
/// The
- private JpegColorSpace DeduceJpegColorSpace()
+ private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
{
- if (this.ComponentCount == 1)
+ if (componentCount == 1)
{
return JpegColorSpace.Grayscale;
}
- if (this.ComponentCount == 3)
+ if (componentCount == 3)
{
if (!this.adobe.Equals(default) && this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
{
@@ -404,14 +367,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return JpegColorSpace.YCbCr;
}
- if (this.ComponentCount == 4)
+ if (componentCount == 4)
{
return this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck
? JpegColorSpace.Ycck
: JpegColorSpace.Cmyk;
}
- JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {this.ComponentCount}");
+ JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {componentCount}");
return default;
}
@@ -550,7 +513,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length.");
}
- var profile = new byte[remaining];
+ byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker))
@@ -584,14 +547,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return;
}
- var identifier = new byte[Icclength];
+ byte[] identifier = new byte[Icclength];
stream.Read(identifier, 0, Icclength);
remaining -= Icclength; // We have read it by this point
if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker))
{
this.isIcc = true;
- var profile = new byte[remaining];
+ byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
if (this.iccData is null)
@@ -629,7 +592,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker))
{
- var resourceBlockData = new byte[remaining];
+ byte[] resourceBlockData = new byte[remaining];
stream.Read(resourceBlockData, 0, remaining);
Span blockDataSpan = resourceBlockData.AsSpan();
@@ -644,8 +607,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
Span imageResourceBlockId = blockDataSpan.Slice(0, 2);
if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker))
{
- var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
- var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
+ int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
+ int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize)
{
@@ -656,8 +619,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
else
{
- var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
- var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
+ int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
+ int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (blockDataSpan.Length < dataStartIdx + resourceDataSize)
{
@@ -680,7 +643,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private static int ReadImageResourceNameLength(Span blockDataSpan)
{
byte nameLength = blockDataSpan[2];
- var nameDataSize = nameLength == 0 ? 2 : nameLength;
+ int nameDataSize = nameLength == 0 ? 2 : nameLength;
if (nameDataSize % 2 != 0)
{
nameDataSize++;
@@ -697,9 +660,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// The block length.
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength)
- {
- return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
- }
+ => BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
///
/// Processes the application header containing the Adobe identifier
@@ -834,58 +795,62 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported.");
}
- // Read initial marker definitions.
+ // Read initial marker definitions
const int length = 6;
stream.Read(this.temp, 0, length);
- // We only support 8-bit and 12-bit precision.
- if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1)
+ // 1 byte: Bits/sample precision
+ byte precision = this.temp[0];
+
+ // Validate: only 8-bit and 12-bit precisions are supported
+ if (Array.IndexOf(this.supportedPrecisions, precision) == -1)
{
JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision supported.");
}
- this.Precision = this.temp[0];
+ // 2 byte: Height
+ int frameHeight = (this.temp[1] << 8) | this.temp[2];
- this.Frame = new JpegFrame
- {
- Extended = frameMarker.Marker == JpegConstants.Markers.SOF1,
- Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2,
- Precision = this.temp[0],
- Scanlines = (this.temp[1] << 8) | this.temp[2],
- SamplesPerLine = (this.temp[3] << 8) | this.temp[4],
- ComponentCount = this.temp[5]
- };
-
- if (this.Frame.SamplesPerLine == 0 || this.Frame.Scanlines == 0)
+ // 2 byte: Width
+ int frameWidth = (this.temp[3] << 8) | this.temp[4];
+
+ // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that)
+ if (frameHeight == 0 || frameWidth == 0)
{
- JpegThrowHelper.ThrowInvalidImageDimensions(this.Frame.SamplesPerLine, this.Frame.Scanlines);
+ JpegThrowHelper.ThrowInvalidImageDimensions(frameWidth, frameHeight);
}
- this.ImageSizeInPixels = new Size(this.Frame.SamplesPerLine, this.Frame.Scanlines);
- this.ComponentCount = this.Frame.ComponentCount;
+ // 1 byte: Number of components
+ byte componentCount = this.temp[5];
+ this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
+
+ this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
+
+ this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount);
if (!metadataOnly)
{
remaining -= length;
+ // Validate: remaining part must be equal to components * 3
const int componentBytes = 3;
- if (remaining > this.ComponentCount * componentBytes)
+ if (remaining != componentCount * componentBytes)
{
JpegThrowHelper.ThrowBadMarker("SOFn", remaining);
}
+ // components*3 bytes: component data
stream.Read(this.temp, 0, remaining);
// No need to pool this. They max out at 4
- this.Frame.ComponentIds = new byte[this.ComponentCount];
- this.Frame.ComponentOrder = new byte[this.ComponentCount];
- this.Frame.Components = new JpegComponent[this.ComponentCount];
- this.ColorSpace = this.DeduceJpegColorSpace();
+ this.Frame.ComponentIds = new byte[componentCount];
+ this.Frame.ComponentOrder = new byte[componentCount];
+ this.Frame.Components = new JpegComponent[componentCount];
int maxH = 0;
int maxV = 0;
int index = 0;
- for (int i = 0; i < this.ComponentCount; i++)
+ for (int i = 0; i < componentCount; i++)
{
byte hv = this.temp[index + 1];
int h = (hv >> 4) & 15;
@@ -909,12 +874,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index += componentBytes;
}
- this.Frame.MaxHorizontalFactor = maxH;
- this.Frame.MaxVerticalFactor = maxV;
- this.ColorSpace = this.DeduceJpegColorSpace();
- this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
- this.Frame.InitComponents();
- this.ImageSizeInMCU = new Size(this.Frame.McusPerLine, this.Frame.McusPerColumn);
+ this.Frame.Init(maxH, maxV);
+
+ this.scanDecoder.InjectFrameData(this.Frame, this);
}
}
@@ -928,9 +890,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
int length = remaining;
- using (IManagedByteBuffer huffmanData = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean))
+ using (IMemoryOwner huffmanData = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean))
{
- ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanData.GetSpan());
+ Span huffmanDataSpan = huffmanData.GetSpan();
+ ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan);
for (int i = 2; i < remaining;)
{
byte huffmanTableSpec = (byte)stream.ReadByte();
@@ -949,11 +912,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Bad Huffman Table index.");
}
- stream.Read(huffmanData.Array, 0, 16);
+ stream.Read(huffmanDataSpan, 0, 16);
- using (IManagedByteBuffer codeLengths = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(17, AllocationOptions.Clean))
+ using (IMemoryOwner codeLengths = this.Configuration.MemoryAllocator.Allocate(17, AllocationOptions.Clean))
{
- ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengths.GetSpan());
+ Span codeLengthsSpan = codeLengths.GetSpan();
+ ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengthsSpan);
int codeLengthSum = 0;
for (int j = 1; j < 17; j++)
@@ -968,17 +932,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length.");
}
- using (IManagedByteBuffer huffmanValues = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean))
+ using (IMemoryOwner huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean))
{
- stream.Read(huffmanValues.Array, 0, codeLengthSum);
+ Span huffmanValuesSpan = huffmanValues.GetSpan();
+ stream.Read(huffmanValuesSpan, 0, codeLengthSum);
i += 17 + codeLengthSum;
- this.BuildHuffmanTable(
- tableType == 0 ? this.dcHuffmanTables : this.acHuffmanTables,
+ this.scanDecoder.BuildHuffmanTable(
+ tableType,
tableIndex,
- codeLengths.GetSpan(),
- huffmanValues.GetSpan());
+ codeLengthsSpan,
+ huffmanValuesSpan);
}
}
}
@@ -998,80 +963,101 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining);
}
- this.resetInterval = this.ReadUint16(stream);
+ this.scanDecoder.ResetInterval = this.ReadUint16(stream);
}
///
/// Processes the SOS (Start of scan marker).
///
- private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationToken cancellationToken)
+ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken)
{
if (this.Frame is null)
{
JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found.");
}
+ // 1 byte: Number of components in scan
int selectorsCount = stream.ReadByte();
- for (int i = 0; i < selectorsCount; i++)
+
+ // Validate: 0 < count <= totalComponents
+ if (selectorsCount == 0 || selectorsCount > this.Frame.ComponentCount)
{
- int componentIndex = -1;
- int selector = stream.ReadByte();
+ // TODO: extract as separate method?
+ JpegThrowHelper.ThrowInvalidImageContentException($"Invalid number of components in scan: {selectorsCount}.");
+ }
+ // Validate: marker must contain exactly (4 + selectorsCount*2) bytes
+ int selectorsBytes = selectorsCount * 2;
+ if (remaining != 4 + selectorsBytes)
+ {
+ JpegThrowHelper.ThrowBadMarker("SOS", remaining);
+ }
+
+ // selectorsCount*2 bytes: component index + huffman tables indices
+ stream.Read(this.temp, 0, selectorsBytes);
+
+ this.Frame.MultiScan = this.Frame.ComponentCount != selectorsCount;
+ for (int i = 0; i < selectorsBytes; i += 2)
+ {
+ // 1 byte: Component id
+ int componentSelectorId = this.temp[i];
+
+ int componentIndex = -1;
for (int j = 0; j < this.Frame.ComponentIds.Length; j++)
{
byte id = this.Frame.ComponentIds[j];
- if (selector == id)
+ if (componentSelectorId == id)
{
componentIndex = j;
break;
}
}
- if (componentIndex < 0)
+ // Validate: must be found among registered components
+ if (componentIndex == -1)
+ {
+ // TODO: extract as separate method?
+ JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component id in scan: {componentSelectorId}.");
+ }
+
+ this.Frame.ComponentOrder[i / 2] = (byte)componentIndex;
+
+ JpegComponent component = this.Frame.Components[componentIndex];
+
+ // 1 byte: Huffman table selectors.
+ // 4 bits - dc
+ // 4 bits - ac
+ int tableSpec = this.temp[i + 1];
+ int dcTableIndex = tableSpec >> 4;
+ int acTableIndex = tableSpec & 15;
+
+ // Validate: both must be < 4
+ if (dcTableIndex >= 4 || acTableIndex >= 4)
{
- JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component selector {componentIndex}.");
+ // TODO: extract as separate method?
+ JpegThrowHelper.ThrowInvalidImageContentException($"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}");
}
- ref JpegComponent component = ref this.Frame.Components[componentIndex];
- int tableSpec = stream.ReadByte();
- component.DCHuffmanTableId = tableSpec >> 4;
- component.ACHuffmanTableId = tableSpec & 15;
- this.Frame.ComponentOrder[i] = (byte)componentIndex;
+ component.DCHuffmanTableId = dcTableIndex;
+ component.ACHuffmanTableId = acTableIndex;
}
+ // 3 bytes: Progressive scan decoding data
stream.Read(this.temp, 0, 3);
int spectralStart = this.temp[0];
+ this.scanDecoder.SpectralStart = spectralStart;
+
int spectralEnd = this.temp[1];
+ this.scanDecoder.SpectralEnd = spectralEnd;
+
int successiveApproximation = this.temp[2];
+ this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4;
+ this.scanDecoder.SuccessiveLow = successiveApproximation & 15;
- var sd = new HuffmanScanDecoder(
- stream,
- this.Frame,
- this.dcHuffmanTables,
- this.acHuffmanTables,
- selectorsCount,
- this.resetInterval,
- spectralStart,
- spectralEnd,
- successiveApproximation >> 4,
- successiveApproximation & 15,
- cancellationToken);
-
- sd.ParseEntropyCodedData();
+ this.scanDecoder.ParseEntropyCodedData(selectorsCount);
}
- ///
- /// Builds the huffman tables
- ///
- /// The tables
- /// The table index
- /// The codelengths
- /// The values
- [MethodImpl(InliningOptions.ShortMethod)]
- private void BuildHuffmanTable(HuffmanTable[] tables, int index, ReadOnlySpan codeLengths, ReadOnlySpan values)
- => tables[index] = new HuffmanTable(codeLengths, values);
-
///
/// Reads a from the stream advancing it by two bytes
///
@@ -1083,32 +1069,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.markerBuffer, 0, 2);
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}
-
- ///
- /// Post processes the pixels into the destination image.
- ///
- /// The pixel format.
- /// The .
- private Image PostProcessIntoImage(CancellationToken cancellationToken)
- where TPixel : unmanaged, IPixel
- {
- if (this.ImageWidth == 0 || this.ImageHeight == 0)
- {
- JpegThrowHelper.ThrowInvalidImageDimensions(this.ImageWidth, this.ImageHeight);
- }
-
- var image = Image.CreateUninitialized(
- this.Configuration,
- this.ImageWidth,
- this.ImageHeight,
- this.Metadata);
-
- using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this))
- {
- postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken);
- }
-
- return image;
- }
}
}
diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
index 0ab141397..83c638934 100644
--- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
@@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode(Span scanline, Span previousScanline, int bytesPerPixel)
{
- DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// The bytes per pixel.
/// The sum of the total variance of the filtered row
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Encode(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum)
+ public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
index e8e0aa704..6a89a1122 100644
--- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
@@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode(Span scanline, Span previousScanline, int bytesPerPixel)
{
- DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// The bytes per pixel.
/// The sum of the total variance of the filtered row
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Encode(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum)
+ public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
index 116154836..c28b877e4 100644
--- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
@@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// The bytes per pixel.
/// The sum of the total variance of the filtered row
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Encode(Span scanline, Span result, int bytesPerPixel, out int sum)
+ public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
index e0f35293a..7e0286991 100644
--- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
@@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode(Span scanline, Span previousScanline)
{
- DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// The filtered scanline result.
/// The sum of the total variance of the filtered row
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Encode(Span scanline, Span previousScanline, Span result, out int sum)
+ public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs
index fd11ba1b6..7b5f390f1 100644
--- a/src/ImageSharp/Formats/Png/PngChunk.cs
+++ b/src/ImageSharp/Formats/Png/PngChunk.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using SixLabors.ImageSharp.Memory;
+using System.Buffers;
namespace SixLabors.ImageSharp.Formats.Png
{
@@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
internal readonly struct PngChunk
{
- public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null)
+ public PngChunk(int length, PngChunkType type, IMemoryOwner data = null)
{
this.Length = length;
this.Type = type;
@@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets the data bytes appropriate to the chunk type, if any.
/// This field can be of zero length or null.
///
- public IManagedByteBuffer Data { get; }
+ public IMemoryOwner Data { get; }
///
/// Gets a value indicating whether the given chunk is critical to decoding
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index c2c336c03..987dc150c 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
@@ -84,12 +85,12 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Previous scanline processed.
///
- private IManagedByteBuffer previousScanline;
+ private IMemoryOwner previousScanline;
///
/// The current scanline that is being processed.
///
- private IManagedByteBuffer scanline;
+ private IMemoryOwner scanline;
///
/// The index of the current scanline being processed.
@@ -149,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type)
{
case PngChunkType.Header:
- this.ReadHeaderChunk(pngMetadata, chunk.Data.Array);
+ this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Physical:
this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan());
@@ -168,29 +169,29 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
case PngChunkType.Palette:
var pal = new byte[chunk.Length];
- Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length);
+ chunk.Data.GetSpan().CopyTo(pal);
this.palette = pal;
break;
case PngChunkType.Transparency:
var alpha = new byte[chunk.Length];
- Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length);
+ chunk.Data.GetSpan().CopyTo(alpha);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha, pngMetadata);
break;
case PngChunkType.Text:
- this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.CompressedText:
- this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.InternationalText:
- this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
{
var exifData = new byte[chunk.Length];
- Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length);
+ chunk.Data.GetSpan().CopyTo(exifData);
metadata.ExifProfile = new ExifProfile(exifData);
}
@@ -239,7 +240,7 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type)
{
case PngChunkType.Header:
- this.ReadHeaderChunk(pngMetadata, chunk.Data.Array);
+ this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Physical:
this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan());
@@ -251,19 +252,19 @@ namespace SixLabors.ImageSharp.Formats.Png
this.SkipChunkDataAndCrc(chunk);
break;
case PngChunkType.Text:
- this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.CompressedText:
- this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.InternationalText:
- this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
{
var exifData = new byte[chunk.Length];
- Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length);
+ chunk.Data.GetSpan().CopyTo(exifData);
metadata.ExifProfile = new ExifProfile(exifData);
}
@@ -312,7 +313,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The number of bits per value.
/// The new array.
/// The resulting array.
- private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IManagedByteBuffer buffer)
+ private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IMemoryOwner buffer)
{
if (bits >= 8)
{
@@ -320,9 +321,9 @@ namespace SixLabors.ImageSharp.Formats.Png
return false;
}
- buffer = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline * 8 / bits, AllocationOptions.Clean);
+ buffer = this.memoryAllocator.Allocate(bytesPerScanline * 8 / bits, AllocationOptions.Clean);
ref byte sourceRef = ref MemoryMarshal.GetReference(source);
- ref byte resultRef = ref buffer.Array[0];
+ ref byte resultRef = ref buffer.GetReference();
int mask = 0xFF >> (8 - bits);
int resultOffset = 0;
@@ -392,8 +393,8 @@ namespace SixLabors.ImageSharp.Formats.Png
this.bytesPerSample = this.header.BitDepth / 8;
}
- this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
- this.scanline = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
+ this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
+ this.scanline = this.Configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
}
///
@@ -504,15 +505,19 @@ namespace SixLabors.ImageSharp.Formats.Png
{
while (this.currentRow < this.header.Height)
{
- int bytesRead = compressedStream.Read(this.scanline.Array, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
- this.currentRowBytesRead += bytesRead;
- if (this.currentRowBytesRead < this.bytesPerScanline)
+ Span scanlineSpan = this.scanline.GetSpan();
+ while (this.currentRowBytesRead < this.bytesPerScanline)
{
- return;
+ int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
+ if (bytesRead <= 0)
+ {
+ return;
+ }
+
+ this.currentRowBytesRead += bytesRead;
}
this.currentRowBytesRead = 0;
- Span scanlineSpan = this.scanline.GetSpan();
switch ((FilterType)scanlineSpan[0])
{
@@ -542,7 +547,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata);
- this.SwapBuffers();
+ this.SwapScanlineBuffers();
this.currentRow++;
}
}
@@ -576,11 +581,15 @@ namespace SixLabors.ImageSharp.Formats.Png
while (this.currentRow < this.header.Height)
{
- int bytesRead = compressedStream.Read(this.scanline.Array, this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead);
- this.currentRowBytesRead += bytesRead;
- if (this.currentRowBytesRead < bytesPerInterlaceScanline)
+ while (this.currentRowBytesRead < bytesPerInterlaceScanline)
{
- return;
+ int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead);
+ if (bytesRead <= 0)
+ {
+ return;
+ }
+
+ this.currentRowBytesRead += bytesRead;
}
this.currentRowBytesRead = 0;
@@ -617,7 +626,7 @@ namespace SixLabors.ImageSharp.Formats.Png
Span rowSpan = image.GetPixelRowSpan(this.currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
- this.SwapBuffers();
+ this.SwapScanlineBuffers();
this.currentRow += Adam7.RowIncrement[pass];
}
@@ -653,70 +662,80 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline - 1, this.header.BitDepth, out IManagedByteBuffer buffer)
- ? buffer.GetSpan()
- : trimmed;
-
- switch (this.pngColorType)
+ IMemoryOwner buffer = null;
+ try
{
- case PngColorType.Grayscale:
- PngScanlineProcessor.ProcessGrayscaleScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentL16.GetValueOrDefault(),
- pngMetadata.TransparentL8.GetValueOrDefault());
+ ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
+ trimmed,
+ this.bytesPerScanline - 1,
+ this.header.BitDepth,
+ out buffer)
+ ? buffer.GetSpan()
+ : trimmed;
+
+ switch (this.pngColorType)
+ {
+ case PngColorType.Grayscale:
+ PngScanlineProcessor.ProcessGrayscaleScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pngMetadata.HasTransparency,
+ pngMetadata.TransparentL16.GetValueOrDefault(),
+ pngMetadata.TransparentL8.GetValueOrDefault());
- break;
+ break;
- case PngColorType.GrayscaleWithAlpha:
- PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- this.bytesPerPixel,
- this.bytesPerSample);
+ case PngColorType.GrayscaleWithAlpha:
+ PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ this.bytesPerPixel,
+ this.bytesPerSample);
- break;
+ break;
- case PngColorType.Palette:
- PngScanlineProcessor.ProcessPaletteScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- this.palette,
- this.paletteAlpha);
+ case PngColorType.Palette:
+ PngScanlineProcessor.ProcessPaletteScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ this.palette,
+ this.paletteAlpha);
- break;
+ break;
- case PngColorType.Rgb:
- PngScanlineProcessor.ProcessRgbScanline(
- this.Configuration,
- this.header,
- scanlineSpan,
- rowSpan,
- this.bytesPerPixel,
- this.bytesPerSample,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentRgb48.GetValueOrDefault(),
- pngMetadata.TransparentRgb24.GetValueOrDefault());
+ case PngColorType.Rgb:
+ PngScanlineProcessor.ProcessRgbScanline(
+ this.Configuration,
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ this.bytesPerPixel,
+ this.bytesPerSample,
+ pngMetadata.HasTransparency,
+ pngMetadata.TransparentRgb48.GetValueOrDefault(),
+ pngMetadata.TransparentRgb24.GetValueOrDefault());
- break;
+ break;
- case PngColorType.RgbWithAlpha:
- PngScanlineProcessor.ProcessRgbaScanline(
- this.Configuration,
- this.header,
- scanlineSpan,
- rowSpan,
- this.bytesPerPixel,
- this.bytesPerSample);
+ case PngColorType.RgbWithAlpha:
+ PngScanlineProcessor.ProcessRgbaScanline(
+ this.Configuration,
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ this.bytesPerPixel,
+ this.bytesPerSample);
- break;
+ break;
+ }
+ }
+ finally
+ {
+ buffer?.Dispose();
}
-
- buffer?.Dispose();
}
///
@@ -735,78 +754,88 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline, this.header.BitDepth, out IManagedByteBuffer buffer)
- ? buffer.GetSpan()
- : trimmed;
-
- switch (this.pngColorType)
+ IMemoryOwner buffer = null;
+ try
{
- case PngColorType.Grayscale:
- PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pixelOffset,
- increment,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentL16.GetValueOrDefault(),
- pngMetadata.TransparentL8.GetValueOrDefault());
+ ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
+ trimmed,
+ this.bytesPerScanline,
+ this.header.BitDepth,
+ out buffer)
+ ? buffer.GetSpan()
+ : trimmed;
+
+ switch (this.pngColorType)
+ {
+ case PngColorType.Grayscale:
+ PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pixelOffset,
+ increment,
+ pngMetadata.HasTransparency,
+ pngMetadata.TransparentL16.GetValueOrDefault(),
+ pngMetadata.TransparentL8.GetValueOrDefault());
- break;
+ break;
- case PngColorType.GrayscaleWithAlpha:
- PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pixelOffset,
- increment,
- this.bytesPerPixel,
- this.bytesPerSample);
+ case PngColorType.GrayscaleWithAlpha:
+ PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pixelOffset,
+ increment,
+ this.bytesPerPixel,
+ this.bytesPerSample);
- break;
+ break;
- case PngColorType.Palette:
- PngScanlineProcessor.ProcessInterlacedPaletteScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pixelOffset,
- increment,
- this.palette,
- this.paletteAlpha);
+ case PngColorType.Palette:
+ PngScanlineProcessor.ProcessInterlacedPaletteScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pixelOffset,
+ increment,
+ this.palette,
+ this.paletteAlpha);
- break;
+ break;
- case PngColorType.Rgb:
- PngScanlineProcessor.ProcessInterlacedRgbScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pixelOffset,
- increment,
- this.bytesPerPixel,
- this.bytesPerSample,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentRgb48.GetValueOrDefault(),
- pngMetadata.TransparentRgb24.GetValueOrDefault());
+ case PngColorType.Rgb:
+ PngScanlineProcessor.ProcessInterlacedRgbScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pixelOffset,
+ increment,
+ this.bytesPerPixel,
+ this.bytesPerSample,
+ pngMetadata.HasTransparency,
+ pngMetadata.TransparentRgb48.GetValueOrDefault(),
+ pngMetadata.TransparentRgb24.GetValueOrDefault());
- break;
+ break;
- case PngColorType.RgbWithAlpha:
- PngScanlineProcessor.ProcessInterlacedRgbaScanline(
- this.header,
- scanlineSpan,
- rowSpan,
- pixelOffset,
- increment,
- this.bytesPerPixel,
- this.bytesPerSample);
+ case PngColorType.RgbWithAlpha:
+ PngScanlineProcessor.ProcessInterlacedRgbaScanline(
+ this.header,
+ scanlineSpan,
+ rowSpan,
+ pixelOffset,
+ increment,
+ this.bytesPerPixel,
+ this.bytesPerSample);
- break;
+ break;
+ }
+ }
+ finally
+ {
+ buffer?.Dispose();
}
-
- buffer?.Dispose();
}
///
@@ -1189,12 +1218,12 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// The length of the chunk data to read.
[MethodImpl(InliningOptions.ShortMethod)]
- private IManagedByteBuffer ReadChunkData(int length)
+ private IMemoryOwner ReadChunkData(int length)
{
// We rent the buffer here to return it afterwards in Decode()
- IManagedByteBuffer buffer = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean);
+ IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean);
- this.currentStream.Read(buffer.Array, 0, length);
+ this.currentStream.Read(buffer.GetSpan(), 0, length);
return buffer;
}
@@ -1272,9 +1301,9 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
- private void SwapBuffers()
+ private void SwapScanlineBuffers()
{
- IManagedByteBuffer temp = this.previousScanline;
+ IMemoryOwner temp = this.previousScanline;
this.previousScanline = this.scanline;
this.scanline = temp;
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 7a285eb70..4f6fb7356 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -80,32 +80,12 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// The raw data of previous scanline.
///
- private IManagedByteBuffer previousScanline;
+ private IMemoryOwner previousScanline;
///
/// The raw data of current scanline.
///
- private IManagedByteBuffer currentScanline;
-
- ///
- /// The common buffer for the filters.
- ///
- private IManagedByteBuffer filterBuffer;
-
- ///
- /// The ext buffer for the sub filter, .
- ///
- private IManagedByteBuffer subFilter;
-
- ///
- /// The ext buffer for the average filter, .
- ///
- private IManagedByteBuffer averageFilter;
-
- ///
- /// The ext buffer for the Paeth filter, .
- ///
- private IManagedByteBuffer paethFilter;
+ private IMemoryOwner currentScanline;
///
/// Initializes a new instance of the class.
@@ -173,17 +153,8 @@ namespace SixLabors.ImageSharp.Formats.Png
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
- this.subFilter?.Dispose();
- this.averageFilter?.Dispose();
- this.paethFilter?.Dispose();
- this.filterBuffer?.Dispose();
-
this.previousScanline = null;
this.currentScanline = null;
- this.subFilter = null;
- this.averageFilter = null;
- this.paethFilter = null;
- this.filterBuffer = null;
}
///
@@ -278,21 +249,17 @@ namespace SixLabors.ImageSharp.Formats.Png
else
{
// 1, 2, and 4 bit grayscale
- using (IManagedByteBuffer temp = this.memoryAllocator.AllocateManagedByteBuffer(
- rowSpan.Length,
- AllocationOptions.Clean))
- {
- int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(this.bitDepth) - 1);
- Span tempSpan = temp.GetSpan();
-
- // We need to first create an array of luminance bytes then scale them down to the correct bit depth.
- PixelOperations.Instance.ToL8Bytes(
- this.configuration,
- rowSpan,
- tempSpan,
- rowSpan.Length);
- PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
- }
+ using IMemoryOwner temp = this.memoryAllocator.Allocate(rowSpan.Length, AllocationOptions.Clean);
+ int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(this.bitDepth) - 1);
+ Span tempSpan = temp.GetSpan();
+
+ // We need to first create an array of luminance bytes then scale them down to the correct bit depth.
+ PixelOperations.Instance.ToL8Bytes(
+ this.configuration,
+ rowSpan,
+ tempSpan,
+ rowSpan.Length);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
}
}
}
@@ -444,6 +411,8 @@ namespace SixLabors.ImageSharp.Formats.Png
case PngColorType.GrayscaleWithAlpha:
this.CollectGrayscaleBytes(rowSpan);
break;
+ case PngColorType.Rgb:
+ case PngColorType.RgbWithAlpha:
default:
this.CollectTPixelBytes(rowSpan);
break;
@@ -451,124 +420,127 @@ namespace SixLabors.ImageSharp.Formats.Png
}
///
- /// Apply filter for the raw scanline.
+ /// Apply the line filter for the raw scanline to enable better compression.
///
- private IManagedByteBuffer FilterPixelBytes()
+ private void FilterPixelBytes(ref Span filter, ref Span attempt)
{
switch (this.options.FilterMethod)
{
case PngFilterMethod.None:
- NoneFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan());
- return this.filterBuffer;
-
+ NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
+ break;
case PngFilterMethod.Sub:
- SubFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
- return this.filterBuffer;
+ SubFilter.Encode(this.currentScanline.GetSpan(), filter, this.bytesPerPixel, out int _);
+ break;
case PngFilterMethod.Up:
- UpFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), out int _);
- return this.filterBuffer;
+ UpFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, out int _);
+ break;
case PngFilterMethod.Average:
- AverageFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
- return this.filterBuffer;
+ AverageFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, this.bytesPerPixel, out int _);
+ break;
case PngFilterMethod.Paeth:
- PaethFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), this.filterBuffer.GetSpan(), this.bytesPerPixel, out int _);
- return this.filterBuffer;
-
+ PaethFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, this.bytesPerPixel, out int _);
+ break;
+ case PngFilterMethod.Adaptive:
default:
- return this.GetOptimalFilteredScanline();
+ this.ApplyOptimalFilteredScanline(ref filter, ref attempt);
+ break;
}
}
///
- /// Encodes the pixel data line by line.
- /// Each scanline is encoded in the most optimal manner to improve compression.
+ /// Collects the pixel data line by line for compressing.
+ /// Each scanline is filtered in the most optimal manner to improve compression.
///
/// The pixel format.
/// The row span.
- /// The quantized pixels. Can be null.
- /// The row.
- /// The
- private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row)
+ /// The filtered buffer.
+ /// Used for attempting optimized filtering.
+ /// The quantized pixels. Can be .
+ /// The row number.
+ private void CollectAndFilterPixelRow(
+ ReadOnlySpan rowSpan,
+ ref Span filter,
+ ref Span attempt,
+ IndexedImageFrame quantized,
+ int row)
where TPixel : unmanaged, IPixel
{
this.CollectPixelBytes(rowSpan, quantized, row);
- return this.FilterPixelBytes();
+ this.FilterPixelBytes(ref filter, ref attempt);
}
///
/// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode.
///
- /// The row span.
- private IManagedByteBuffer EncodeAdam7IndexedPixelRow(ReadOnlySpan rowSpan)
+ /// The row span.
+ /// The filtered buffer.
+ /// Used for attempting optimized filtering.
+ private void EncodeAdam7IndexedPixelRow(
+ ReadOnlySpan row,
+ ref Span filter,
+ ref Span attempt)
{
// CollectPixelBytes
if (this.bitDepth < 8)
{
- PngEncoderHelpers.ScaleDownFrom8BitArray(rowSpan, this.currentScanline.GetSpan(), this.bitDepth);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(row, this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
- rowSpan.CopyTo(this.currentScanline.GetSpan());
+ row.CopyTo(this.currentScanline.GetSpan());
}
- return this.FilterPixelBytes();
+ this.FilterPixelBytes(ref filter, ref attempt);
}
///
/// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed
/// to be most compressible, using lowest total variation as proxy for compressibility.
///
- /// The
- private IManagedByteBuffer GetOptimalFilteredScanline()
+ private void ApplyOptimalFilteredScanline(ref Span filter, ref Span attempt)
{
// Palette images don't compress well with adaptive filtering.
- if (this.options.ColorType == PngColorType.Palette || this.bitDepth < 8)
+ // Nor do images comprising a single row.
+ if (this.options.ColorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8)
{
- NoneFilter.Encode(this.currentScanline.GetSpan(), this.filterBuffer.GetSpan());
- return this.filterBuffer;
+ NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
+ return;
}
- this.AllocateExtBuffers();
- Span scanSpan = this.currentScanline.GetSpan();
- Span prevSpan = this.previousScanline.GetSpan();
-
- // This order, while different to the enumerated order is more likely to produce a smaller sum
- // early on which shaves a couple of milliseconds off the processing time.
- UpFilter.Encode(scanSpan, prevSpan, this.filterBuffer.GetSpan(), out int currentSum);
+ Span current = this.currentScanline.GetSpan();
+ Span previous = this.previousScanline.GetSpan();
- // TODO: PERF.. We should be breaking out of the encoding for each line as soon as we hit the sum.
- // That way the above comment would actually be true. It used to be anyway...
- // If we could use SIMD for none branching filters we could really speed it up.
- int lowestSum = currentSum;
- IManagedByteBuffer actualResult = this.filterBuffer;
-
- PaethFilter.Encode(scanSpan, prevSpan, this.paethFilter.GetSpan(), this.bytesPerPixel, out currentSum);
-
- if (currentSum < lowestSum)
+ int min = int.MaxValue;
+ SubFilter.Encode(current, attempt, this.bytesPerPixel, out int sum);
+ if (sum < min)
{
- lowestSum = currentSum;
- actualResult = this.paethFilter;
+ min = sum;
+ SwapSpans(ref filter, ref attempt);
}
- SubFilter.Encode(scanSpan, this.subFilter.GetSpan(), this.bytesPerPixel, out currentSum);
-
- if (currentSum < lowestSum)
+ UpFilter.Encode(current, previous, attempt, out sum);
+ if (sum < min)
{
- lowestSum = currentSum;
- actualResult = this.subFilter;
+ min = sum;
+ SwapSpans(ref filter, ref attempt);
}
- AverageFilter.Encode(scanSpan, prevSpan, this.averageFilter.GetSpan(), this.bytesPerPixel, out currentSum);
-
- if (currentSum < lowestSum)
+ AverageFilter.Encode(current, previous, attempt, this.bytesPerPixel, out sum);
+ if (sum < min)
{
- actualResult = this.averageFilter;
+ min = sum;
+ SwapSpans(ref filter, ref attempt);
}
- return actualResult;
+ PaethFilter.Encode(current, previous, attempt, this.bytesPerPixel, out sum);
+ if (sum < min)
+ {
+ SwapSpans(ref filter, ref attempt);
+ }
}
///
@@ -612,8 +584,8 @@ namespace SixLabors.ImageSharp.Formats.Png
int colorTableLength = paletteLength * Unsafe.SizeOf();
bool hasAlpha = false;
- using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
- using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
+ using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength);
+ using IMemoryOwner alphaTable = this.memoryAllocator.Allocate(paletteLength);
ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan()));
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
@@ -640,12 +612,12 @@ namespace SixLabors.ImageSharp.Formats.Png
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
- this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
+ this.WriteChunk(stream, PngChunkType.Palette, colorTable.GetSpan(), 0, colorTableLength);
// Write the transparency data
if (hasAlpha)
{
- this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
+ this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.GetSpan(), 0, paletteLength);
}
}
@@ -924,38 +896,13 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Allocates the buffers for each scanline.
///
/// The bytes per scanline.
- /// Length of the result.
- private void AllocateBuffers(int bytesPerScanline, int resultLength)
+ private void AllocateScanlineBuffers(int bytesPerScanline)
{
// Clean up from any potential previous runs.
- this.subFilter?.Dispose();
- this.averageFilter?.Dispose();
- this.paethFilter?.Dispose();
- this.subFilter = null;
- this.averageFilter = null;
- this.paethFilter = null;
-
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
- this.filterBuffer?.Dispose();
- this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline, AllocationOptions.Clean);
- this.currentScanline = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline, AllocationOptions.Clean);
- this.filterBuffer = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- }
-
- ///
- /// Allocates the ext buffers for adaptive filter.
- ///
- private void AllocateExtBuffers()
- {
- if (this.subFilter == null)
- {
- int resultLength = this.filterBuffer.Length();
-
- this.subFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- this.averageFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- this.paethFilter = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
- }
+ this.previousScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean);
+ this.currentScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean);
}
///
@@ -969,17 +916,19 @@ namespace SixLabors.ImageSharp.Formats.Png
where TPixel : unmanaged, IPixel
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
- int resultLength = bytesPerScanline + 1;
- this.AllocateBuffers(bytesPerScanline, resultLength);
+ int filterLength = bytesPerScanline + 1;
+ this.AllocateScanlineBuffers(bytesPerScanline);
+ using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
+ using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
+
+ Span filter = filterBuffer.GetSpan();
+ Span attempt = attemptBuffer.GetSpan();
for (int y = 0; y < this.height; y++)
{
- IManagedByteBuffer r = this.EncodePixelRow(pixels.GetPixelRowSpan(y), quantized, y);
- deflateStream.Write(r.Array, 0, resultLength);
-
- IManagedByteBuffer temp = this.currentScanline;
- this.currentScanline = this.previousScanline;
- this.previousScanline = temp;
+ this.CollectAndFilterPixelRow(pixels.GetPixelRowSpan(y), ref filter, ref attempt, quantized, y);
+ deflateStream.Write(filter);
+ this.SwapScanlineBuffers();
}
}
@@ -1004,36 +953,33 @@ namespace SixLabors.ImageSharp.Formats.Png
? ((blockWidth * this.bitDepth) + 7) / 8
: blockWidth * this.bytesPerPixel;
- int resultLength = bytesPerScanline + 1;
+ int filterLength = bytesPerScanline + 1;
+ this.AllocateScanlineBuffers(bytesPerScanline);
- this.AllocateBuffers(bytesPerScanline, resultLength);
+ using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth);
+ using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
+ using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
- using (IMemoryOwner passData = this.memoryAllocator.Allocate(blockWidth))
+ Span block = blockBuffer.GetSpan();
+ Span filter = filterBuffer.GetSpan();
+ Span attempt = attemptBuffer.GetSpan();
+
+ for (int row = startRow; row < height; row += Adam7.RowIncrement[pass])
{
- Span destSpan = passData.Memory.Span;
- for (int row = startRow;
- row < height;
- row += Adam7.RowIncrement[pass])
+ // Collect pixel data
+ Span srcRow = pixels.GetPixelRowSpan(row);
+ for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass])
{
- // collect data
- Span srcRow = pixels.GetPixelRowSpan(row);
- for (int col = startCol, i = 0;
- col < width;
- col += Adam7.ColumnIncrement[pass])
- {
- destSpan[i++] = srcRow[col];
- }
+ block[i++] = srcRow[col];
+ }
- // encode data
- // note: quantized parameter not used
- // note: row parameter not used
- IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)destSpan, null, -1);
- deflateStream.Write(r.Array, 0, resultLength);
+ // Encode data
+ // Note: quantized parameter not used
+ // Note: row parameter not used
+ this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1);
+ deflateStream.Write(filter);
- IManagedByteBuffer temp = this.currentScanline;
- this.currentScanline = this.previousScanline;
- this.previousScanline = temp;
- }
+ this.SwapScanlineBuffers();
}
}
}
@@ -1059,34 +1005,36 @@ namespace SixLabors.ImageSharp.Formats.Png
? ((blockWidth * this.bitDepth) + 7) / 8
: blockWidth * this.bytesPerPixel;
- int resultLength = bytesPerScanline + 1;
+ int filterLength = bytesPerScanline + 1;
- this.AllocateBuffers(bytesPerScanline, resultLength);
+ this.AllocateScanlineBuffers(bytesPerScanline);
- using (IMemoryOwner passData = this.memoryAllocator.Allocate(blockWidth))
+ using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth);
+ using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
+ using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
+
+ Span block = blockBuffer.GetSpan();
+ Span filter = filterBuffer.GetSpan();
+ Span attempt = attemptBuffer.GetSpan();
+
+ for (int row = startRow;
+ row < height;
+ row += Adam7.RowIncrement[pass])
{
- Span destSpan = passData.Memory.Span;
- for (int row = startRow;
- row < height;
- row += Adam7.RowIncrement[pass])
+ // Collect data
+ ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row);
+ for (int col = startCol, i = 0;
+ col < width;
+ col += Adam7.ColumnIncrement[pass])
{
- // collect data
- ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row);
- for (int col = startCol, i = 0;
- col < width;
- col += Adam7.ColumnIncrement[pass])
- {
- destSpan[i++] = srcRow[col];
- }
+ block[i++] = srcRow[col];
+ }
- // encode data
- IManagedByteBuffer r = this.EncodeAdam7IndexedPixelRow(destSpan);
- deflateStream.Write(r.Array, 0, resultLength);
+ // Encode data
+ this.EncodeAdam7IndexedPixelRow(block, ref filter, ref attempt);
+ deflateStream.Write(filter);
- IManagedByteBuffer temp = this.currentScanline;
- this.currentScanline = this.previousScanline;
- this.previousScanline = temp;
- }
+ this.SwapScanlineBuffers();
}
}
}
@@ -1103,7 +1051,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The to write to.
/// The type of chunk to write.
/// The containing data.
- private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
+ private void WriteChunk(Stream stream, PngChunkType type, Span data)
+ => this.WriteChunk(stream, type, data, 0, data.Length);
///
/// Writes a chunk of a specified length to the stream at the given offset.
@@ -1113,7 +1062,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The containing data.
/// The position to offset the data at.
/// The of the data to write.
- private void WriteChunk(Stream stream, PngChunkType type, byte[] data, int offset, int length)
+ private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length)
{
BinaryPrimitives.WriteInt32BigEndian(this.buffer, length);
BinaryPrimitives.WriteUInt32BigEndian(this.buffer.AsSpan(4, 4), (uint)type);
@@ -1126,7 +1075,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
stream.Write(data, offset, length);
- crc = Crc32.Calculate(crc, data.AsSpan(offset, length));
+ crc = Crc32.Calculate(crc, data.Slice(offset, length));
}
BinaryPrimitives.WriteUInt32BigEndian(this.buffer, crc);
@@ -1154,5 +1103,19 @@ namespace SixLabors.ImageSharp.Formats.Png
return scanlineLength / mod;
}
+
+ private void SwapScanlineBuffers()
+ {
+ IMemoryOwner temp = this.previousScanline;
+ this.previousScanline = this.currentScanline;
+ this.currentScanline = temp;
+ }
+
+ private static void SwapSpans(ref Span a, ref Span b)
+ {
+ Span t = b;
+ b = a;
+ a = t;
+ }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
index 67af4ff6c..2188913bc 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
@@ -46,7 +46,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
deframeStream.AllocateNewBytes(byteCount, true);
DeflateStream dataStream = deframeStream.CompressedStream;
- dataStream.Read(buffer, 0, buffer.Length);
+
+ int totalRead = 0;
+ while (totalRead < buffer.Length)
+ {
+ int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead);
+ if (bytesRead <= 0)
+ {
+ break;
+ }
+
+ totalRead += bytesRead;
+ }
}
if (this.Predictor == TiffPredictor.Horizontal)
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs
new file mode 100644
index 000000000..635be95f4
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers.Binary;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
+{
+ ///
+ /// Implements the 'RGB' photometric interpretation with 16 bits for each channel.
+ ///
+ internal class Rgb161616TiffColor : TiffBaseColorDecoder
+ where TPixel : unmanaged, IPixel
+ {
+ private readonly bool isBigEndian;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// if set to true decodes the pixel data as big endian, otherwise as little endian.
+ public Rgb161616TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian;
+
+ ///
+ public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
+ {
+ var color = default(TPixel);
+
+ int offset = 0;
+
+ var rgba = default(Rgba64);
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.GetRowSpan(y);
+
+ for (int x = left; x < left + width; x++)
+ {
+ ulong r = this.ConvertToShort(data.Slice(offset, 2));
+ offset += 2;
+ ulong g = this.ConvertToShort(data.Slice(offset, 2));
+ offset += 2;
+ ulong b = this.ConvertToShort(data.Slice(offset, 2));
+ offset += 2;
+
+ rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48);
+ color.FromRgba64(rgba);
+
+ pixelRow[x] = color;
+ }
+ }
+ }
+
+ private ushort ConvertToShort(ReadOnlySpan buffer) => this.isBigEndian
+ ? BinaryPrimitives.ReadUInt16BigEndian(buffer)
+ : BinaryPrimitives.ReadUInt16LittleEndian(buffer);
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
index 36d2ab746..8e711d3eb 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
@@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
internal static class TiffColorDecoderFactory
where TPixel : unmanaged, IPixel
{
- public static TiffBaseColorDecoder Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap)
+ public static TiffBaseColorDecoder Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder)
{
switch (colorType)
{
@@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
&& bitsPerSample.Channel0 == 16,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
- return new RgbTiffColor(bitsPerSample);
+ return new Rgb161616TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian);
case TiffColorType.PaletteColor:
DebugGuard.NotNull(colorMap, "colorMap");
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 3d5bfc737..484e182c5 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -36,6 +36,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
private BufferedReadStream inputStream;
+ ///
+ /// Indicates the byte order of the stream.
+ ///
+ private ByteOrder byteOrder;
+
///
/// Initializes a new instance of the class.
///
@@ -109,6 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var reader = new DirectoryReader(stream);
IEnumerable directories = reader.Read();
+ this.byteOrder = reader.ByteOrder;
var frames = new List>();
foreach (ExifProfile ifd in directories)
@@ -310,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.Predictor,
this.FaxCompressionOptions);
- TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap);
+ TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder);
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
index 662e729ef..6c96e4fc3 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
@@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
{
// Write uncompressed image.
int bytesPerStrip = this.BytesPerRow * height;
- this.bitStrip ??= this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerStrip);
+ this.bitStrip ??= this.MemoryAllocator.Allocate(bytesPerStrip);
this.pixelsAsGray ??= this.MemoryAllocator.Allocate(width);
Span pixelAsGraySpan = this.pixelsAsGray.GetSpan();
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
index 61e24d652..e95236fd2 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
@@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
else
{
int stripPixels = width * height;
- this.indexedPixelsBuffer ??= this.MemoryAllocator.AllocateManagedByteBuffer(stripPixels);
+ this.indexedPixelsBuffer ??= this.MemoryAllocator.Allocate(stripPixels);
Span indexedPixels = this.indexedPixelsBuffer.GetSpan();
int lastRow = y + height;
int indexedPixelsRowIdx = 0;
@@ -113,7 +113,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
private void AddColorMapTag()
{
- using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(this.colorPaletteBytes);
+ using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.Allocate(this.colorPaletteBytes);
Span colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan quantizedColors = this.quantizedImage.Palette.Span;
diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs
index b43ff0422..2aa9c5394 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -87,6 +87,21 @@ namespace SixLabors.ImageSharp
this.frames = new ImageFrameCollection(this, width, height, default(TPixel));
}
+ ///
+ /// Initializes a new instance of the class
+ /// wrapping an external pixel bufferx.
+ ///
+ /// The configuration providing initialization code which allows extending the library.
+ /// Pixel buffer.
+ /// The images metadata.
+ internal Image(
+ Configuration configuration,
+ Buffer2D pixelBuffer,
+ ImageMetadata metadata)
+ : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata)
+ {
+ }
+
///
/// Initializes a new instance of the class
/// wrapping an external .
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
index ff376a618..af56b99a0 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Memory
protected internal abstract int GetBufferCapacityInBytes();
///
- /// Allocates an , holding a of length .
+ /// Allocates an , holding a of length .
///
/// Type of the data stored in the buffer.
/// Size of the buffer to allocate.
diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets
index 9c1788145..ddceaff1f 100644
--- a/tests/Directory.Build.targets
+++ b/tests/Directory.Build.targets
@@ -18,17 +18,21 @@
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
index 68a102e3c..9db666c37 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
@@ -4,6 +4,7 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Tests;
using SDSize = System.Drawing.Size;
@@ -39,21 +40,46 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream);
var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true });
- decoder.ParseStream(bufferedStream);
+ var scanDecoder = new HuffmanScanDecoder(bufferedStream, new NoopSpectralConverter(), cancellationToken: default);
+ decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
decoder.Dispose();
}
- }
- /*
- | Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
- |---------------------------- |----------- |-------------- |--------------------- |---------:|----------:|----------:|------:|--------:|------:|------:|----------:|
- | 'System.Drawing FULL' | Job-HITJFX | .NET 4.7.2 | Jpg/b(...)e.jpg [21] | 5.828 ms | 0.9885 ms | 0.0542 ms | 1.00 | 46.8750 | - | - | 211566 B |
- | JpegDecoderCore.ParseStream | Job-HITJFX | .NET 4.7.2 | Jpg/b(...)e.jpg [21] | 5.833 ms | 0.2923 ms | 0.0160 ms | 1.00 | - | - | - | 12416 B |
- | | | | | | | | | | | | |
- | 'System.Drawing FULL' | Job-WPSKZD | .NET Core 2.1 | Jpg/b(...)e.jpg [21] | 6.018 ms | 2.1374 ms | 0.1172 ms | 1.00 | 46.8750 | - | - | 210768 B |
- | JpegDecoderCore.ParseStream | Job-WPSKZD | .NET Core 2.1 | Jpg/b(...)e.jpg [21] | 4.382 ms | 0.9009 ms | 0.0494 ms | 0.73 | - | - | - | 12360 B |
- | | | | | | | | | | | | |
- | 'System.Drawing FULL' | Job-ZLSNRP | .NET Core 3.1 | Jpg/b(...)e.jpg [21] | 5.714 ms | 0.4078 ms | 0.0224 ms | 1.00 | - | - | - | 176 B |
- | JpegDecoderCore.ParseStream | Job-ZLSNRP | .NET Core 3.1 | Jpg/b(...)e.jpg [21] | 4.239 ms | 1.0943 ms | 0.0600 ms | 0.74 | - | - | - | 12406 B |
- */
+ // We want to test only stream parsing and scan decoding, we don't need to convert spectral data to actual pixels
+ // Nor we need to allocate final pixel buffer
+ // Note: this still introduces virtual method call overhead for baseline interleaved images
+ // There's no way to eliminate it as spectral conversion is built into the scan decoding loop for memory footprint reduction
+ private class NoopSpectralConverter : SpectralConverter
+ {
+ public override void ConvertStrideBaseline()
+ {
+ }
+
+ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
+ {
+ }
+ }
+ }
}
+
+/*
+BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.1083 (20H2/October2020Update)
+Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
+.NET SDK=6.0.100-preview.3.21202.5
+ [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT
+ Job-VAJCIU : .NET Core 2.1.26 (CoreCLR 4.6.29812.02, CoreFX 4.6.29812.01), X64 RyuJIT
+ Job-INPXCR : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT
+ Job-JRCLOJ : .NET Framework 4.8 (4.8.4390.0), X64 RyuJIT
+
+IterationCount=3 LaunchCount=1 WarmupCount=3
+| Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
+|---------------------------- |----------- |--------------------- |---------------------- |---------:|----------:|----------:|------:|--------:|------:|------:|----------:|
+| 'System.Drawing FULL' | Job-VAJCIU | .NET Core 2.1 | Jpg/baseline/Lake.jpg | 5.196 ms | 0.7520 ms | 0.0412 ms | 1.00 | 46.8750 | - | - | 210,768 B |
+| JpegDecoderCore.ParseStream | Job-VAJCIU | .NET Core 2.1 | Jpg/baseline/Lake.jpg | 3.467 ms | 0.0784 ms | 0.0043 ms | 0.67 | - | - | - | 12,416 B |
+| | | | | | | | | | | | |
+| 'System.Drawing FULL' | Job-INPXCR | .NET Core 3.1 | Jpg/baseline/Lake.jpg | 5.201 ms | 0.4105 ms | 0.0225 ms | 1.00 | - | - | - | 183 B |
+| JpegDecoderCore.ParseStream | Job-INPXCR | .NET Core 3.1 | Jpg/baseline/Lake.jpg | 3.349 ms | 0.0468 ms | 0.0026 ms | 0.64 | - | - | - | 12,408 B |
+| | | | | | | | | | | | |
+| 'System.Drawing FULL' | Job-JRCLOJ | .NET Framework 4.7.2 | Jpg/baseline/Lake.jpg | 5.164 ms | 0.6524 ms | 0.0358 ms | 1.00 | 46.8750 | - | - | 211,571 B |
+| JpegDecoderCore.ParseStream | Job-JRCLOJ | .NET Framework 4.7.2 | Jpg/baseline/Lake.jpg | 4.548 ms | 0.3357 ms | 0.0184 ms | 0.88 | - | - | - | 12,480 B |
+*/
diff --git a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs
index 914041e5b..fcb3daf3b 100644
--- a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs
+++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs
@@ -4,10 +4,10 @@
using BenchmarkDotNet.Attributes;
using Colourful;
-using Colourful.Conversion;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
+using Illuminants = Colourful.Illuminants;
namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
{
@@ -19,12 +19,12 @@ namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter();
- private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter();
+ private static readonly IColorConverter ColourfulConverter = new ConverterBuilder().FromXYZ(Illuminants.D50).ToLab(Illuminants.D50).Build();
[Benchmark(Baseline = true, Description = "Colourful Convert")]
public double ColourfulConvert()
{
- return ColourfulConverter.ToLab(XYZColor).L;
+ return ColourfulConverter.Convert(XYZColor).L;
}
[Benchmark(Description = "ImageSharp Convert")]
diff --git a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs
index c6f4c0471..afba44e73 100644
--- a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs
+++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs
@@ -4,10 +4,10 @@
using BenchmarkDotNet.Attributes;
using Colourful;
-using Colourful.Conversion;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
+using Illuminants = Colourful.Illuminants;
namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
{
@@ -19,12 +19,12 @@ namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter();
- private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter();
+ private static readonly IColorConverter ColourfulConverter = new ConverterBuilder().FromXYZ(Illuminants.C).ToHunterLab(Illuminants.C).Build();
[Benchmark(Baseline = true, Description = "Colourful Convert")]
public double ColourfulConvert()
{
- return ColourfulConverter.ToHunterLab(XYZColor).L;
+ return ColourfulConverter.Convert(XYZColor).L;
}
[Benchmark(Description = "ImageSharp Convert")]
diff --git a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs
index c7f78bb08..eddc1a680 100644
--- a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs
+++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs
@@ -4,7 +4,6 @@
using BenchmarkDotNet.Attributes;
using Colourful;
-using Colourful.Conversion;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
@@ -19,12 +18,12 @@ namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter();
- private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter();
+ private static readonly IColorConverter ColourfulConverter = new ConverterBuilder().FromXYZ().ToLMS().Build();
[Benchmark(Baseline = true, Description = "Colourful Convert")]
public double ColourfulConvert()
{
- return ColourfulConverter.ToLMS(XYZColor).L;
+ return ColourfulConverter.Convert(XYZColor).L;
}
[Benchmark(Description = "ImageSharp Convert")]
diff --git a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs
index 18494f3f6..b56e55b1e 100644
--- a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs
+++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs
@@ -4,7 +4,6 @@
using BenchmarkDotNet.Attributes;
using Colourful;
-using Colourful.Conversion;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
@@ -19,12 +18,12 @@ namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter();
- private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter();
+ private static readonly IColorConverter ColourfulConverter = new ConverterBuilder().FromXYZ(RGBWorkingSpaces.sRGB.WhitePoint).ToRGB(RGBWorkingSpaces.sRGB).Build();
[Benchmark(Baseline = true, Description = "Colourful Convert")]
public double ColourfulConvert()
{
- return ColourfulConverter.ToRGB(XYZColor).R;
+ return ColourfulConverter.Convert(XYZColor).R;
}
[Benchmark(Description = "ImageSharp Convert")]
diff --git a/tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs b/tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs
index 21cf10bb7..d42b22ecb 100644
--- a/tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs
+++ b/tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs
@@ -4,7 +4,6 @@
using BenchmarkDotNet.Attributes;
using Colourful;
-using Colourful.Conversion;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
@@ -15,20 +14,20 @@ namespace SixLabors.ImageSharp.Benchmarks.ColorSpaces
{
private static readonly Rgb Rgb = new Rgb(0.206162F, 0.260277F, 0.746717F, RgbWorkingSpaces.WideGamutRgb);
- private static readonly RGBColor RGBColor = new RGBColor(0.206162, 0.260277, 0.746717, RGBWorkingSpaces.WideGamutRGB);
+ private static readonly RGBColor RGBColor = new RGBColor(0.206162, 0.260277, 0.746717);
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter(new ColorSpaceConverterOptions { TargetRgbWorkingSpace = RgbWorkingSpaces.SRgb });
- private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter { TargetRGBWorkingSpace = RGBWorkingSpaces.sRGB };
+ private static readonly IColorConverter ColourfulConverter = new ConverterBuilder().FromRGB(RGBWorkingSpaces.WideGamutRGB).ToRGB(RGBWorkingSpaces.sRGB).Build();
[Benchmark(Baseline = true, Description = "Colourful Adapt")]
public RGBColor ColourfulConvert()
{
- return ColourfulConverter.Adapt(RGBColor);
+ return ColourfulConverter.Convert(RGBColor);
}
[Benchmark(Description = "ImageSharp Adapt")]
- internal Rgb ColorSpaceConvert()
+ public Rgb ColorSpaceConvert()
{
return ColorSpaceConverter.Adapt(Rgb);
}
diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
index 17f6068d4..84b83ee14 100644
--- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
+++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
@@ -9,7 +9,7 @@
false
Debug;Release;Debug-InnerLoop;Release-InnerLoop
-
+ 9
@@ -38,8 +38,13 @@
+
+
+
+
+
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
new file mode 100644
index 000000000..f1f7de3dc
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using BenchmarkDotNet.Attributes;
+
+namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
+{
+ // See README.md for instructions about initialization.
+ [MemoryDiagnoser]
+ [ShortRunJob]
+ public class LoadResizeSaveStressBenchmarks
+ {
+ private LoadResizeSaveStressRunner runner;
+
+ // private const JpegKind Filter = JpegKind.Progressive;
+ private const JpegKind Filter = JpegKind.Any;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this.runner = new LoadResizeSaveStressRunner()
+ {
+ ImageCount = Environment.ProcessorCount,
+ Filter = Filter
+ };
+ Console.WriteLine($"ImageCount: {this.runner.ImageCount} Filter: {Filter}");
+ this.runner.Init();
+ }
+
+ private void ForEachImage(Action action, int maxDegreeOfParallelism)
+ {
+ this.runner.MaxDegreeOfParallelism = maxDegreeOfParallelism;
+ this.runner.ForEachImageParallel(action);
+ }
+
+ public int[] ParallelismValues { get; } =
+ {
+ Environment.ProcessorCount,
+ Environment.ProcessorCount / 2,
+ Environment.ProcessorCount / 4,
+ 1
+ };
+
+ [Benchmark(Baseline = true)]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void SystemDrawing(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.SystemDrawingResize, maxDegreeOfParallelism);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void Magick(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.MagickResize, maxDegreeOfParallelism);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void MagicScaler(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.MagicScalerResize, maxDegreeOfParallelism);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void SkiaBitmap(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.SkiaBitmapResize, maxDegreeOfParallelism);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(ParallelismValues))]
+ public void NetVips(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.NetVipsResize, maxDegreeOfParallelism);
+ }
+}
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
new file mode 100644
index 000000000..c15f641b4
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
@@ -0,0 +1,280 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using ImageMagick;
+using PhotoSauce.MagicScaler;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Tests;
+using SkiaSharp;
+using ImageSharpImage = SixLabors.ImageSharp.Image;
+using ImageSharpSize = SixLabors.ImageSharp.Size;
+using NetVipsImage = NetVips.Image;
+using SystemDrawingImage = System.Drawing.Image;
+
+namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
+{
+ public enum JpegKind
+ {
+ Baseline = 1,
+ Progressive = 2,
+ Any = Baseline | Progressive
+ }
+
+ public class LoadResizeSaveStressRunner
+ {
+ private const int ThumbnailSize = 150;
+ private const int Quality = 75;
+ private const string ImageSharp = nameof(ImageSharp);
+ private const string SystemDrawing = nameof(SystemDrawing);
+ private const string MagickNET = nameof(MagickNET);
+ private const string NetVips = nameof(NetVips);
+ private const string MagicScaler = nameof(MagicScaler);
+ private const string SkiaSharpCanvas = nameof(SkiaSharpCanvas);
+ private const string SkiaSharpBitmap = nameof(SkiaSharpBitmap);
+
+ // Set the quality for ImagSharp
+ private readonly JpegEncoder imageSharpJpegEncoder = new () { Quality = Quality };
+ private readonly ImageCodecInfo systemDrawingJpegCodec =
+ ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
+
+ public string[] Images { get; private set; }
+
+ public double TotalProcessedMegapixels { get; private set; }
+
+ private string outputDirectory;
+
+ public int ImageCount { get; set; } = int.MaxValue;
+
+ public int MaxDegreeOfParallelism { get; set; } = -1;
+
+ public JpegKind Filter { get; set; }
+
+ private static readonly string[] ProgressiveFiles =
+ {
+ "ancyloscelis-apiformis-m-paraguay-face_2014-08-08-095255-zs-pmax_15046500892_o.jpg",
+ "acanthopus-excellens-f-face-brasil_2014-08-06-132105-zs-pmax_14792513890_o.jpg",
+ "bee-ceratina-monster-f-ukraine-face_2014-08-09-123342-zs-pmax_15068816101_o.jpg",
+ "bombus-eximias-f-tawain-face_2014-08-10-094449-zs-pmax_15155452565_o.jpg",
+ "ceratina-14507h1-m-vietnam-face_2014-08-09-163218-zs-pmax_15096718245_o.jpg",
+ "ceratina-buscki-f-panama-face_2014-11-25-140413-zs-pmax_15923736081_o.jpg",
+ "ceratina-tricolor-f-panama-face2_2014-08-29-160402-zs-pmax_14906318297_o.jpg",
+ "ceratina-tricolor-f-panama-face_2014-08-29-160001-zs-pmax_14906300608_o.jpg",
+ "ceratina-tricolor-m-panama-face_2014-08-29-162821-zs-pmax_15069878876_o.jpg",
+ "coelioxys-cayennensis-f-argentina-face_2014-08-09-171932-zs-pmax_14914109737_o.jpg",
+ "ctenocolletes-smaragdinus-f-australia-face_2014-08-08-134825-zs-pmax_14865269708_o.jpg",
+ "diphaglossa-gayi-f-face-chile_2014-08-04-180547-zs-pmax_14918891472_o.jpg",
+ "hylaeus-nubilosus-f-australia-face_2014-08-14-121100-zs-pmax_15049602149_o.jpg",
+ "hypanthidioides-arenaria-f-face-brazil_2014-08-06-061201-zs-pmax_14770371360_o.jpg",
+ "megachile-chalicodoma-species-f-morocco-face_2014-08-14-124840-zs-pmax_15217084686_o.jpg",
+ "megachile-species-f-15266b06-face-kenya_2014-08-06-161044-zs-pmax_14994381392_o.jpg",
+ "megalopta-genalis-m-face-panama-barocolorado_2014-09-19-164939-zs-pmax_15121397069_o.jpg",
+ "melitta-haemorrhoidalis-m--england-face_2014-11-02-014026-zs-pmax-recovered_15782113675_o.jpg",
+ "nomia-heart-antennae-m-15266b02-face-kenya_2014-08-04-195216-zs-pmax_14922843736_o.jpg",
+ "nomia-species-m-oman-face_2014-08-09-192602-zs-pmax_15128732411_o.jpg",
+ "nomia-spiney-m-vietnam-face_2014-08-09-213126-zs-pmax_15191389705_o.jpg",
+ "ochreriades-fasciata-m-face-israel_2014-08-06-084407-zs-pmax_14965515571_o.jpg",
+ "osmia-brevicornisf-jaw-kyrgystan_2014-08-08-103333-zs-pmax_14865267787_o.jpg",
+ "pachyanthidium-aff-benguelense-f-6711f07-face_2014-08-07-112830-zs-pmax_15018069042_o.jpg",
+ "pachymelus-bicolor-m-face-madagascar_2014-08-06-134930-zs-pmax_14801667477_o.jpg",
+ "psaenythia-species-m-argentina-face_2014-08-07-163754-zs-pmax_15007018976_o.jpg",
+ "stingless-bee-1-f-face-peru_2014-07-30-123322-zs-pmax_15633797167_o.jpg",
+ "triepeolus-simplex-m-face-md-kent-county_2014-07-22-100937-zs-pmax_14805405233_o.jpg",
+ "washed-megachile-f-face-chile_2014-08-06-103414-zs-pmax_14977843152_o.jpg",
+ "xylocopa-balck-violetwing-f-kyrgystan-angle_2014-08-09-182433-zs-pmax_15123416061_o.jpg",
+ "xylocopa-india-yellow-m-india-face_2014-08-10-111701-zs-pmax_15166559172_o.jpg",
+ };
+
+ public void Init()
+ {
+ if (RuntimeInformation.OSArchitecture is Architecture.X86 or Architecture.X64)
+ {
+ // Workaround ImageMagick issue
+ OpenCL.IsEnabled = false;
+ }
+
+ string imageDirectory = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "MemoryStress");
+ if (!Directory.Exists(imageDirectory) || !Directory.EnumerateFiles(imageDirectory).Any())
+ {
+ throw new DirectoryNotFoundException($"Copy stress images to: {imageDirectory}");
+ }
+
+ // Get at most this.ImageCount images from there
+ bool FilterFunc(string f) => this.Filter.HasFlag(GetJpegType(f));
+
+ this.Images = Directory.EnumerateFiles(imageDirectory).Where(FilterFunc).Take(this.ImageCount).ToArray();
+
+ // Create the output directory next to the images directory
+ this.outputDirectory = TestEnvironment.CreateOutputDirectory("MemoryStress");
+
+ static JpegKind GetJpegType(string f) =>
+ ProgressiveFiles.Any(p => f.EndsWith(p, StringComparison.OrdinalIgnoreCase))
+ ? JpegKind.Progressive
+ : JpegKind.Baseline;
+ }
+
+ public void ForEachImageParallel(Action action) => Parallel.ForEach(
+ this.Images,
+ new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism },
+ action);
+
+ private void IncreaseTotalMegapixels(int width, int height)
+ {
+ double pixels = width * (double)height;
+ this.TotalProcessedMegapixels += pixels / 1_000_000.0;
+ }
+
+ private string OutputPath(string inputPath, string postfix) =>
+ Path.Combine(
+ this.outputDirectory,
+ Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath));
+
+ private (int width, int height) ScaledSize(int inWidth, int inHeight, int outSize)
+ {
+ int width, height;
+ if (inWidth > inHeight)
+ {
+ width = outSize;
+ height = (int)Math.Round(inHeight * outSize / (double)inWidth);
+ }
+ else
+ {
+ width = (int)Math.Round(inWidth * outSize / (double)inHeight);
+ height = outSize;
+ }
+
+ return (width, height);
+ }
+
+ public void SystemDrawingResize(string input)
+ {
+ using var image = SystemDrawingImage.FromFile(input, true);
+ this.IncreaseTotalMegapixels(image.Width, image.Height);
+
+ (int width, int height) scaled = this.ScaledSize(image.Width, image.Height, ThumbnailSize);
+ var resized = new Bitmap(scaled.width, scaled.height);
+ using var graphics = Graphics.FromImage(resized);
+ using var attributes = new ImageAttributes();
+ attributes.SetWrapMode(WrapMode.TileFlipXY);
+ graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ graphics.CompositingMode = CompositingMode.SourceCopy;
+ graphics.CompositingQuality = CompositingQuality.AssumeLinear;
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
+
+ // Save the results
+ using var encoderParams = new EncoderParameters(1);
+ using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality);
+ encoderParams.Param[0] = qualityParam;
+ resized.Save(this.OutputPath(input, SystemDrawing), this.systemDrawingJpegCodec, encoderParams);
+ }
+
+ public void ImageSharpResize(string input)
+ {
+ using FileStream output = File.Open(this.OutputPath(input, ImageSharp), FileMode.Create);
+
+ // Resize it to fit a 150x150 square
+ using var image = ImageSharpImage.Load(input);
+ this.IncreaseTotalMegapixels(image.Width, image.Height);
+
+ image.Mutate(i => i.Resize(new ResizeOptions
+ {
+ Size = new ImageSharpSize(ThumbnailSize, ThumbnailSize),
+ Mode = ResizeMode.Max
+ }));
+
+ // Reduce the size of the file
+ image.Metadata.ExifProfile = null;
+
+ // Save the results
+ image.Save(output, this.imageSharpJpegEncoder);
+ }
+
+ public void MagickResize(string input)
+ {
+ using var image = new MagickImage(input);
+ this.IncreaseTotalMegapixels(image.Width, image.Height);
+
+ // Resize it to fit a 150x150 square
+ image.Resize(ThumbnailSize, ThumbnailSize);
+
+ // Reduce the size of the file
+ image.Strip();
+
+ // Set the quality
+ image.Quality = Quality;
+
+ // Save the results
+ image.Write(this.OutputPath(input, MagickNET));
+ }
+
+ public void MagicScalerResize(string input)
+ {
+ var settings = new ProcessImageSettings()
+ {
+ Width = ThumbnailSize,
+ Height = ThumbnailSize,
+ ResizeMode = CropScaleMode.Max,
+ SaveFormat = FileFormat.Jpeg,
+ JpegQuality = Quality,
+ JpegSubsampleMode = ChromaSubsampleMode.Subsample420
+ };
+
+ // TODO: Is there a way to capture input dimensions for IncreaseTotalMegapixels?
+ using var output = new FileStream(this.OutputPath(input, MagicScaler), FileMode.Create);
+ MagicImageProcessor.ProcessImage(input, output, settings);
+ }
+
+ public void SkiaCanvasResize(string input)
+ {
+ using var original = SKBitmap.Decode(input);
+ this.IncreaseTotalMegapixels(original.Width, original.Height);
+ (int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize);
+ using var surface = SKSurface.Create(new SKImageInfo(scaled.width, scaled.height, original.ColorType, original.AlphaType));
+ using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High };
+ SKCanvas canvas = surface.Canvas;
+ canvas.Scale((float)scaled.width / original.Width);
+ canvas.DrawBitmap(original, 0, 0, paint);
+ canvas.Flush();
+
+ using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpCanvas));
+ surface.Snapshot()
+ .Encode(SKEncodedImageFormat.Jpeg, Quality)
+ .SaveTo(output);
+ }
+
+ public void SkiaBitmapResize(string input)
+ {
+ using var original = SKBitmap.Decode(input);
+ this.IncreaseTotalMegapixels(original.Width, original.Height);
+ (int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize);
+ using var resized = original.Resize(new SKImageInfo(scaled.width, scaled.height), SKFilterQuality.High);
+ if (resized == null)
+ {
+ return;
+ }
+
+ using var image = SKImage.FromBitmap(resized);
+ using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpBitmap));
+ image.Encode(SKEncodedImageFormat.Jpeg, Quality)
+ .SaveTo(output);
+ }
+
+ public void NetVipsResize(string input)
+ {
+ // Thumbnail to fit a 150x150 square
+ using var thumb = NetVipsImage.Thumbnail(input, ThumbnailSize, ThumbnailSize);
+
+ // Save the results
+ thumb.Jpegsave(this.OutputPath(input, NetVips), q: Quality, strip: true);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/README.md b/tests/ImageSharp.Benchmarks/LoadResizeSave/README.md
new file mode 100644
index 000000000..6cb48eb48
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/README.md
@@ -0,0 +1,9 @@
+The benchmarks have been adapted from the
+[PhotoSauce's MemoryStress project](https://github.com/saucecontrol/core-imaging-playground/tree/beeees/MemoryStress).
+
+### Setup
+
+Download the [Bee Heads album](https://www.flickr.com/photos/usgsbiml/albums/72157633925491877) from the USGS Bee Inventory flickr
+ and extract to folder `\tests\Images\ActualOutput\MemoryStress\`.
+
+
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
index a60ac604f..10deb24c6 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
@@ -14,6 +14,7 @@
false
Debug;Release;Debug-InnerLoop;Release-InnerLoop
false
+ 9
@@ -31,11 +32,17 @@
+
+
+
+
+
+
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
new file mode 100644
index 000000000..2aadf02eb
--- /dev/null
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Diagnostics;
+using System.Text;
+using SixLabors.ImageSharp.Benchmarks.LoadResizeSave;
+
+namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
+{
+ // See ImageSharp.Benchmarks/LoadResizeSave/README.md
+ internal class LoadResizeSaveParallelMemoryStress
+ {
+ private readonly LoadResizeSaveStressRunner benchmarks;
+
+ private LoadResizeSaveParallelMemoryStress()
+ {
+ this.benchmarks = new LoadResizeSaveStressRunner()
+ {
+ // MaxDegreeOfParallelism = 10,
+ // Filter = JpegKind.Baseline
+ };
+ this.benchmarks.Init();
+ }
+
+ private double TotalProcessedMegapixels => this.benchmarks.TotalProcessedMegapixels;
+
+ public static void Run()
+ {
+ Console.WriteLine(@"Choose a library for image resizing stress test:
+
+1. System.Drawing
+2. ImageSharp
+3. MagicScaler
+4. SkiaSharp
+5. NetVips
+6. ImageMagick
+");
+
+ ConsoleKey key = Console.ReadKey().Key;
+ if (key < ConsoleKey.D1 || key > ConsoleKey.D6)
+ {
+ Console.WriteLine("Unrecognized command.");
+ return;
+ }
+
+ try
+ {
+ var lrs = new LoadResizeSaveParallelMemoryStress();
+
+ Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}");
+ Console.WriteLine($"Running with MaxDegreeOfParallelism={lrs.benchmarks.MaxDegreeOfParallelism} ...");
+ var timer = Stopwatch.StartNew();
+
+ switch (key)
+ {
+ case ConsoleKey.D1:
+ lrs.SystemDrawingBenchmarkParallel();
+ break;
+ case ConsoleKey.D2:
+ lrs.ImageSharpBenchmarkParallel();
+ break;
+ case ConsoleKey.D3:
+ lrs.MagicScalerBenchmarkParallel();
+ break;
+ case ConsoleKey.D4:
+ lrs.SkiaBitmapBenchmarkParallel();
+ break;
+ case ConsoleKey.D5:
+ lrs.NetVipsBenchmarkParallel();
+ break;
+ case ConsoleKey.D6:
+ lrs.MagickBenchmarkParallel();
+ break;
+ }
+
+ timer.Stop();
+ var stats = new Stats(timer, lrs.TotalProcessedMegapixels);
+ Console.WriteLine("Done. TotalProcessedMegapixels: " + lrs.TotalProcessedMegapixels);
+ Console.WriteLine(stats.GetMarkdown());
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.ToString());
+ }
+ }
+
+ private struct Stats
+ {
+ public double TotalSeconds { get; }
+
+ public double TotalMegapixels { get; }
+
+ public double MegapixelsPerSec { get; }
+
+ public double MegapixelsPerSecPerCpu { get; }
+
+ public Stats(Stopwatch sw, double totalMegapixels)
+ {
+ this.TotalMegapixels = totalMegapixels;
+ this.TotalSeconds = sw.ElapsedMilliseconds / 1000.0;
+ this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds;
+ this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / Environment.ProcessorCount;
+ }
+
+ public string GetMarkdown()
+ {
+ var bld = new StringBuilder();
+ bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
+ bld.AppendLine(
+ $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
+
+ bld.Append("| ");
+ bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds);
+ bld.Append(" | ");
+ bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
+ bld.Append(" | ");
+ bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
+ bld.AppendLine(" |");
+
+ return bld.ToString();
+
+ static string L(string header) => new ('-', header.Length);
+ static string F(string column) => $"{{0,{column.Length}:f3}}";
+ }
+ }
+
+ private void ForEachImage(Action action) => this.benchmarks.ForEachImageParallel(action);
+
+ private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize);
+
+ private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize);
+
+ private void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize);
+
+ private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize);
+
+ private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize);
+
+ private void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize);
+ }
+}
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
index 50a930b6f..e6e82b981 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
@@ -31,13 +31,14 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
///
public static void Main(string[] args)
{
- RunJpegEncoderProfilingTests();
+ LoadResizeSaveParallelMemoryStress.Run();
+ // RunJpegEncoderProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest();
// RunResizeProfilingTest();
- Console.ReadLine();
+ // Console.ReadLine();
}
private static void RunJpegEncoderProfilingTests()
diff --git a/tests/ImageSharp.Tests/Common/NumericsTests.cs b/tests/ImageSharp.Tests/Common/NumericsTests.cs
index 29eae6d48..62819af49 100644
--- a/tests/ImageSharp.Tests/Common/NumericsTests.cs
+++ b/tests/ImageSharp.Tests/Common/NumericsTests.cs
@@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Tests.Common
int expected = 0;
int actual = Numerics.Log2(value);
- Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ Assert.Equal(expected, actual);
}
[Fact]
@@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Tests.Common
int expected = i;
int actual = Numerics.Log2(value);
- Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ Assert.Equal(expected, actual);
}
}
@@ -66,7 +66,35 @@ namespace SixLabors.ImageSharp.Tests.Common
int expected = Log2_ReferenceImplementation(value);
int actual = Numerics.Log2(value);
- Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
+ Assert.Equal(expected, actual);
+ }
+ }
+
+ private static uint DivideCeil_ReferenceImplementation(uint value, uint divisor) => (uint)MathF.Ceiling((float)value / divisor);
+
+ [Fact]
+ public void DivideCeil_DivideZero()
+ {
+ uint expected = 0;
+ uint actual = Numerics.DivideCeil(0, 100);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(1, 100)]
+ public void DivideCeil_RandomValues(int seed, int count)
+ {
+ var rng = new Random(seed);
+ for (int i = 0; i < count; i++)
+ {
+ uint value = (uint)rng.Next();
+ uint divisor = (uint)rng.Next();
+
+ uint expected = DivideCeil_ReferenceImplementation(value, divisor);
+ uint actual = Numerics.DivideCeil(value, divisor);
+
+ Assert.True(expected == actual, $"Expected: {expected}\nActual: {actual}\n{value} / {divisor} = {expected}");
}
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 2b42b65f0..e64d8452f 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -20,6 +20,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
+ [Collection("RunSerial")]
[Trait("Format", "Bmp")]
public class BmpDecoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index 90e6cf43f..f338c1aff 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -19,6 +19,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
+ [Collection("RunSerial")]
[Trait("Format", "Bmp")]
public class BmpEncoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index c3250d72c..c0df1e400 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -17,6 +17,7 @@ using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
+ [Collection("RunSerial")]
[Trait("Format", "Gif")]
public class GifDecoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
index 3a0f188ce..bd24e1a8d 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
@@ -14,6 +14,7 @@ using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
+ [Collection("RunSerial")]
[Trait("Format", "Gif")]
public class GifEncoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
index 2faea2611..d12240cba 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
@@ -17,8 +17,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Baseline.Jpeg400,
TestImages.Jpeg.Baseline.Turtle420,
TestImages.Jpeg.Baseline.Testorig420,
-
- // BUG: The following image has a high difference compared to the expected output: 1.0096%
TestImages.Jpeg.Baseline.Jpeg420Small,
TestImages.Jpeg.Issues.Fuzz.AccessViolationException922,
TestImages.Jpeg.Baseline.Jpeg444,
@@ -89,7 +87,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Issues.Fuzz.ArgumentException826B,
TestImages.Jpeg.Issues.Fuzz.ArgumentException826C,
TestImages.Jpeg.Issues.Fuzz.AccessViolationException827,
- TestImages.Jpeg.Issues.Fuzz.ExecutionEngineException839
+ TestImages.Jpeg.Issues.Fuzz.ExecutionEngineException839,
+ TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A,
+ TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B
};
private static readonly Dictionary CustomToleranceValues =
@@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[TestImages.Jpeg.Baseline.Bad.BadRST] = 0.0589f / 100,
[TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100,
- [TestImages.Jpeg.Baseline.Jpeg420Small] = 1.1f / 100,
+ [TestImages.Jpeg.Baseline.Jpeg420Small] = 0.287f / 100,
[TestImages.Jpeg.Baseline.Turtle420] = 1.0f / 100,
// Progressive:
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 67df6a881..674aa6d8f 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -6,7 +6,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@@ -22,6 +21,7 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
// TODO: Scatter test cases into multiple test classes
+ [Collection("RunSerial")]
[Trait("Format", "Jpg")]
public partial class JpegDecoderTests
{
@@ -62,10 +62,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return !TestEnvironment.Is64BitProcess && largeImagesToSkipOn32Bit.Contains(provider.SourceFileOrDescription);
}
- public JpegDecoderTests(ITestOutputHelper output)
- {
- this.Output = output;
- }
+ public JpegDecoderTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@@ -78,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var ms = new MemoryStream(bytes);
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
- decoder.ParseStream(bufferedStream);
+ using Image image = decoder.Decode(bufferedStream, cancellationToken: default);
// I don't know why these numbers are different. All I know is that the decoder works
// and spectral data is exactly correct also.
@@ -131,10 +128,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(0)]
[InlineData(0.5)]
[InlineData(0.9)]
- public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
+ public async Task DecodeAsync_IsCancellable(int percentageOfStreamReadToCancel)
{
var cts = new CancellationTokenSource();
- var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
+ string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
@@ -163,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
var cts = new CancellationTokenSource();
- var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
+ string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 3c48865c7..8e12b04be 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
@@ -20,6 +20,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
+ [Collection("RunSerial")]
[Trait("Format", "Jpg")]
public class JpegEncoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs
deleted file mode 100644
index 93d9aee92..000000000
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using SixLabors.ImageSharp.Formats.Jpeg;
-using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
-using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
-
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SixLabors.ImageSharp.Tests.Formats.Jpg
-{
- [Trait("Format", "Jpg")]
- public class JpegImagePostProcessorTests
- {
- public static string[] BaselineTestJpegs =
- {
- TestImages.Jpeg.Baseline.Calliphora,
- TestImages.Jpeg.Baseline.Cmyk,
- TestImages.Jpeg.Baseline.Ycck,
- TestImages.Jpeg.Baseline.Jpeg400,
- TestImages.Jpeg.Baseline.Testorig420,
- TestImages.Jpeg.Baseline.Jpeg444,
- };
-
- public JpegImagePostProcessorTests(ITestOutputHelper output)
- {
- this.Output = output;
- }
-
- private ITestOutputHelper Output { get; }
-
- private static void SaveBuffer(JpegComponentPostProcessor cp, TestImageProvider provider)
- where TPixel : unmanaged, IPixel
- {
- using (Image image = cp.ColorBuffer.ToGrayscaleImage(1f / 255f))
- {
- image.DebugSave(provider, $"-C{cp.Component.Index}-");
- }
- }
-
- [Theory]
- [WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32)]
- [WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)]
- public void DoProcessorStep(TestImageProvider provider)
- where TPixel : unmanaged, IPixel
- {
- string imageFile = provider.SourceFileOrDescription;
- using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
- using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder))
- using (var imageFrame = new ImageFrame(Configuration.Default, decoder.ImageWidth, decoder.ImageHeight))
- {
- pp.DoPostProcessorStep(imageFrame);
-
- JpegComponentPostProcessor[] cp = pp.ComponentProcessors;
-
- SaveBuffer(cp[0], provider);
- SaveBuffer(cp[1], provider);
- SaveBuffer(cp[2], provider);
- }
- }
-
- [Theory]
- [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
- public void PostProcess(TestImageProvider provider)
- where TPixel : unmanaged, IPixel
- {
- string imageFile = provider.SourceFileOrDescription;
- using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
- using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder))
- using (var image = new Image(decoder.ImageWidth, decoder.ImageHeight))
- {
- pp.PostProcess(image.Frames.RootFrame, default);
-
- image.DebugSave(provider);
-
- ImagingTestCaseUtility testUtil = provider.Utility;
- testUtil.TestGroupName = nameof(JpegDecoderTests);
- testUtil.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName;
-
- using (Image referenceImage =
- provider.GetReferenceOutputImage(appendPixelTypeToFileName: false))
- {
- ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image);
-
- this.Output.WriteLine($"*** {imageFile} ***");
- this.Output.WriteLine($"Difference: {report.DifferencePercentageString}");
-
- // ReSharper disable once PossibleInvalidOperationException
- Assert.True(report.TotalNormalizedDifference.Value < 0.005f);
- }
- }
- }
- }
-}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
index de8103d63..0a4d85344 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
@@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
var expectedColorSpace = (JpegColorSpace)expectedColorSpaceValue;
- using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
+ using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile, metaDataOnly: true))
{
Assert.Equal(expectedColorSpace, decoder.ColorSpace);
}
@@ -43,12 +43,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(TestImages.Jpeg.Baseline.Jpeg400))
{
- Assert.Equal(1, decoder.ComponentCount);
+ Assert.Equal(1, decoder.Frame.ComponentCount);
Assert.Equal(1, decoder.Components.Length);
- Size expectedSizeInBlocks = decoder.ImageSizeInPixels.DivideRoundUp(8);
+ Size expectedSizeInBlocks = decoder.Frame.PixelSize.DivideRoundUp(8);
- Assert.Equal(expectedSizeInBlocks, decoder.ImageSizeInMCU);
+ Assert.Equal(expectedSizeInBlocks, decoder.Frame.McuSize);
var uniform1 = new Size(1, 1);
JpegComponent c0 = decoder.Components[0];
@@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
{
sb.AppendLine(imageFile);
- sb.AppendLine($"Size:{decoder.ImageSizeInPixels} MCU:{decoder.ImageSizeInMCU}");
+ sb.AppendLine($"Size:{decoder.Frame.PixelSize} MCU:{decoder.Frame.McuSize}");
JpegComponent c0 = decoder.Components[0];
JpegComponent c1 = decoder.Components[1];
@@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
{
- Assert.Equal(componentCount, decoder.ComponentCount);
+ Assert.Equal(componentCount, decoder.Frame.ComponentCount);
Assert.Equal(componentCount, decoder.Components.Length);
JpegComponent c0 = decoder.Components[0];
@@ -115,7 +115,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var uniform1 = new Size(1, 1);
- Size expectedLumaSizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(fLuma);
+ Size expectedLumaSizeInBlocks = decoder.Frame.McuSize.MultiplyBy(fLuma);
Size divisor = fLuma.DivideBy(fChroma);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
index 91b1b9cd7..0b819bf13 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
@@ -4,9 +4,12 @@
using System;
using System.IO;
using System.Linq;
-
+using System.Threading;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jpeg.Components;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
@@ -44,20 +47,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
[Theory(Skip = "Debug only, enable manually!")]
+ //[Theory]
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
- var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
-
+ // Calculating data from ImageSharp
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
using var ms = new MemoryStream(sourceBytes);
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
- decoder.ParseStream(bufferedStream);
- var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
- VerifyJpeg.SaveSpectralImage(provider, data);
+ // internal scan decoder which we substitute to assert spectral correctness
+ var debugConverter = new DebugSpectralConverter();
+ var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default);
+
+ // This would parse entire image
+ decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
+ VerifyJpeg.SaveSpectralImage(provider, debugConverter.SpectralData);
}
[Theory]
@@ -70,25 +78,31 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return;
}
- var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
+ // Expected data from libjpeg
+ LibJpegTools.SpectralData libJpegData = LibJpegTools.ExtractSpectralData(provider.SourceFileOrDescription);
+ // Calculating data from ImageSharp
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
using var ms = new MemoryStream(sourceBytes);
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
- decoder.ParseStream(bufferedStream);
- var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder);
- this.VerifySpectralCorrectnessImpl(provider, imageSharpData);
+ // internal scan decoder which we substitute to assert spectral correctness
+ var debugConverter = new DebugSpectralConverter();
+ var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default);
+
+ // This would parse entire image
+ decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
+
+ // Actual verification
+ this.VerifySpectralCorrectnessImpl(libJpegData, debugConverter.SpectralData);
}
- private void VerifySpectralCorrectnessImpl(
- TestImageProvider provider,
+ private void VerifySpectralCorrectnessImpl(
+ LibJpegTools.SpectralData libJpegData,
LibJpegTools.SpectralData imageSharpData)
- where TPixel : unmanaged, IPixel
{
- LibJpegTools.SpectralData libJpegData = LibJpegTools.ExtractSpectralData(provider.SourceFileOrDescription);
-
bool equality = libJpegData.Equals(imageSharpData);
this.Output.WriteLine("Spectral data equality: " + equality);
@@ -108,11 +122,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
LibJpegTools.ComponentData libJpegComponent = libJpegData.Components[i];
LibJpegTools.ComponentData imageSharpComponent = imageSharpData.Components[i];
- (double total, double average) diff = LibJpegTools.CalculateDifference(libJpegComponent, imageSharpComponent);
+ (double total, double average) = LibJpegTools.CalculateDifference(libJpegComponent, imageSharpComponent);
- this.Output.WriteLine($"Component{i}: {diff}");
- averageDifference += diff.average;
- totalDifference += diff.total;
+ this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]");
+ averageDifference += average;
+ totalDifference += total;
tolerance += libJpegComponent.SpectralBlocks.DangerousGetSingleSpan().Length;
}
@@ -126,5 +140,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.True(totalDifference < tolerance);
}
+
+ private class DebugSpectralConverter : SpectralConverter
+ where TPixel : unmanaged, IPixel
+ {
+ private JpegFrame frame;
+
+ private LibJpegTools.SpectralData spectralData;
+
+ private int baselineScanRowCounter;
+
+ public LibJpegTools.SpectralData SpectralData
+ {
+ get
+ {
+ // Due to underlying architecture, baseline interleaved jpegs would inject spectral data during parsing
+ // Progressive and multi-scan images must be loaded manually
+ if (this.frame.Progressive || this.frame.MultiScan)
+ {
+ LibJpegTools.ComponentData[] components = this.spectralData.Components;
+ for (int i = 0; i < components.Length; i++)
+ {
+ components[i].LoadSpectral(this.frame.Components[i]);
+ }
+ }
+
+ return this.spectralData;
+ }
+ }
+
+ public override void ConvertStrideBaseline()
+ {
+ // This would be called only for baseline non-interleaved images
+ // We must copy spectral strides here
+ LibJpegTools.ComponentData[] components = this.spectralData.Components;
+ for (int i = 0; i < components.Length; i++)
+ {
+ components[i].LoadSpectralStride(this.frame.Components[i].SpectralBlocks, this.baselineScanRowCounter);
+ }
+
+ this.baselineScanRowCounter++;
+
+ // As spectral buffers are reused for each stride decoding - we need to manually clear it like it's done in SpectralConverter
+ foreach (JpegComponent component in this.frame.Components)
+ {
+ Buffer2D spectralBlocks = component.SpectralBlocks;
+ for (int i = 0; i < spectralBlocks.Height; i++)
+ {
+ spectralBlocks.GetRowSpan(i).Clear();
+ }
+ }
+ }
+
+ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
+ {
+ this.frame = frame;
+
+ var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount];
+ for (int i = 0; i < spectralComponents.Length; i++)
+ {
+ JpegComponent component = frame.Components[i];
+ spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index);
+ }
+
+ this.spectralData = new LibJpegTools.SpectralData(spectralComponents);
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
new file mode 100644
index 000000000..353ae39f0
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.IO;
+using System.Linq;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Jpg
+{
+ [Trait("Format", "Jpg")]
+ public class SpectralToPixelConversionTests
+ {
+ public static readonly string[] BaselineTestJpegs =
+ {
+ TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk, TestImages.Jpeg.Baseline.Jpeg400,
+ TestImages.Jpeg.Baseline.Jpeg444, TestImages.Jpeg.Baseline.Testorig420,
+ TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Baseline.Bad.BadEOF,
+ TestImages.Jpeg.Baseline.MultiScanBaselineCMYK
+ };
+
+ public SpectralToPixelConversionTests(ITestOutputHelper output)
+ {
+ this.Output = output;
+ }
+
+ private ITestOutputHelper Output { get; }
+
+ [Theory]
+ [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
+ public void Decoder_PixelBufferComparison(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // Stream
+ byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
+ using var ms = new MemoryStream(sourceBytes);
+ using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
+
+ // Decoding
+ using var converter = new SpectralConverter(Configuration.Default, cancellationToken: default);
+ var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
+ var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default);
+ decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
+
+ // Test metadata
+ provider.Utility.TestGroupName = nameof(JpegDecoderTests);
+ provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName;
+
+ // Comparison
+ using (Image image = new Image(Configuration.Default, converter.PixelBuffer, new ImageMetadata()))
+ using (Image referenceImage = provider.GetReferenceOutputImage(appendPixelTypeToFileName: false))
+ {
+ ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image);
+
+ this.Output.WriteLine($"*** {provider.SourceFileOrDescription} ***");
+ this.Output.WriteLine($"Difference: {report.DifferencePercentageString}");
+
+ // ReSharper disable once PossibleInvalidOperationException
+ Assert.True(report.TotalNormalizedDifference.Value < 0.005f);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
index c6f4704f0..ccb7f6f1e 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
@@ -9,6 +9,7 @@ using System.Text;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.PixelFormats;
using Xunit;
using Xunit.Abstractions;
@@ -196,7 +197,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
- decoder.ParseStream(bufferedStream, metaDataOnly);
+ if (metaDataOnly)
+ {
+ decoder.Identify(bufferedStream, cancellationToken: default);
+ }
+ else
+ {
+ using Image image = decoder.Decode(bufferedStream, cancellationToken: default);
+ }
return decoder;
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
index 6f6032ee2..edb8d457b 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
@@ -56,23 +56,48 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
this.SpectralBlocks[x, y] = new Block8x8(data);
}
- public static ComponentData Load(JpegComponent c, int index)
+ public void LoadSpectralStride(Buffer2D data, int strideIndex)
{
- var result = new ComponentData(
- c.WidthInBlocks,
- c.HeightInBlocks,
- index);
+ int startIndex = strideIndex * data.Height;
+
+ int endIndex = Math.Min(this.HeightInBlocks, startIndex + data.Height);
- for (int y = 0; y < result.HeightInBlocks; y++)
+ for (int y = startIndex; y < endIndex; y++)
{
- Span blockRow = c.SpectralBlocks.GetRowSpan(y);
- for (int x = 0; x < result.WidthInBlocks; x++)
+ Span blockRow = data.GetRowSpan(y - startIndex);
+ for (int x = 0; x < this.WidthInBlocks; x++)
{
- short[] data = blockRow[x].ToArray();
- result.MakeBlock(data, y, x);
+ short[] block = blockRow[x].ToArray();
+
+ // x coordinate stays the same - we load entire stride
+ // y coordinate is tricky as we load single stride to full buffer - offset is needed
+ this.MakeBlock(block, y, x);
}
}
+ }
+
+ public void LoadSpectral(JpegComponent c)
+ {
+ Buffer2D data = c.SpectralBlocks;
+ for (int y = 0; y < this.HeightInBlocks; y++)
+ {
+ Span blockRow = data.GetRowSpan(y);
+ for (int x = 0; x < this.WidthInBlocks; x++)
+ {
+ short[] block = blockRow[x].ToArray();
+ this.MakeBlock(block, y, x);
+ }
+ }
+ }
+
+ public static ComponentData Load(JpegComponent c, int index)
+ {
+ var result = new ComponentData(
+ c.WidthInBlocks,
+ c.HeightInBlocks,
+ index);
+ result.LoadSpectral(c);
return result;
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs
index 6ed7c15ae..2d0672f17 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs
@@ -29,14 +29,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
this.Components = components;
}
- public static SpectralData LoadFromImageSharpDecoder(JpegDecoderCore decoder)
- {
- JpegComponent[] srcComponents = decoder.Frame.Components;
- LibJpegTools.ComponentData[] destComponents = srcComponents.Select(LibJpegTools.ComponentData.Load).ToArray();
-
- return new SpectralData(destComponents);
- }
-
public Image TryCreateRGBSpectralImage()
{
if (this.ComponentCount != 3)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 7147f82d6..9832aeb7b 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -16,6 +16,7 @@ using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
+ [Collection("RunSerial")]
[Trait("Format", "Png")]
public partial class PngDecoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 58d733c4f..50bacfba4 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -15,6 +15,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
+ [Collection("RunSerial")]
[Trait("Format", "Png")]
public partial class PngEncoderTests
{
diff --git a/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
index a9b53e16e..be9883a70 100644
--- a/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
@@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// The bytes per pixel.
/// The sum of the total variance of the filtered row
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void EncodePaethFilter(Span scanline, Span