Browse Source

Merge branch 'master' into encode-multiframe

pull/1686/head
Brian Popow 5 years ago
committed by GitHub
parent
commit
ffa8af7dd2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 1
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  3. 4
      src/ImageSharp/Common/Helpers/DebugGuard.cs
  4. 8
      src/ImageSharp/Common/Helpers/Numerics.cs
  5. 2
      src/ImageSharp/Compression/Zlib/Deflater.cs
  6. 41
      src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
  7. 28
      src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
  8. 27
      src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
  9. 31
      src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
  10. 23
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  11. 206
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  12. 17
      src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
  13. 6
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs
  14. 30
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
  15. 64
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
  16. 91
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  17. 181
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs
  18. 34
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  19. 146
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  20. 4
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
  21. 335
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  22. 4
      src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
  23. 4
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  24. 2
      src/ImageSharp/Formats/Png/Filters/SubFilter.cs
  25. 4
      src/ImageSharp/Formats/Png/Filters/UpFilter.cs
  26. 6
      src/ImageSharp/Formats/Png/PngChunk.cs
  27. 327
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  28. 353
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  29. 13
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
  30. 58
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs
  31. 4
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  32. 8
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  33. 2
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  34. 4
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  35. 15
      src/ImageSharp/Image{TPixel}.cs
  36. 4
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  37. 18
      tests/Directory.Build.targets
  38. 54
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  39. 6
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs
  40. 6
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs
  41. 5
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs
  42. 5
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs
  43. 9
      tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs
  44. 7
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  45. 69
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
  46. 280
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  47. 9
      tests/ImageSharp.Benchmarks/LoadResizeSave/README.md
  48. 7
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  49. 142
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  50. 5
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  51. 34
      tests/ImageSharp.Tests/Common/NumericsTests.cs
  52. 1
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  53. 1
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  54. 1
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  55. 1
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  56. 8
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
  57. 15
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  58. 1
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  59. 97
      tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs
  60. 14
      tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
  61. 118
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  62. 69
      tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
  63. 10
      tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
  64. 45
      tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
  65. 8
      tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs
  66. 1
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  67. 1
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  68. 10
      tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
  69. 1
      tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
  70. 1
      tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs
  71. 25
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs
  72. 2
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  73. 1
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  74. 1
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  75. 3
      tests/ImageSharp.Tests/TestImages.cs
  76. 4
      tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png
  77. 3
      tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg
  78. 3
      tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg
  79. 3
      tests/Images/Input/Tiff/Issues/Issue1716.tiff

1
.gitignore

@ -221,4 +221,5 @@ artifacts/
# Tests
**/Images/ActualOutput
**/Images/ReferenceOutput
**/Images/Input/MemoryStress
.DS_Store

1
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
{

4
src/ImageSharp/Common/Helpers/DebugGuard.cs

@ -37,7 +37,7 @@ namespace SixLabors
/// <paramref name="target"/> has a different size than <paramref name="other"/>
/// </exception>
[Conditional("DEBUG")]
public static void MustBeSameSized<T>(Span<T> target, Span<T> other, string parameterName)
public static void MustBeSameSized<T>(ReadOnlySpan<T> target, ReadOnlySpan<T> other, string parameterName)
where T : struct
{
if (target.Length != other.Length)
@ -57,7 +57,7 @@ namespace SixLabors
/// <paramref name="target"/> has less items than <paramref name="minSpan"/>
/// </exception>
[Conditional("DEBUG")]
public static void MustBeSizedAtLeast<T>(Span<T> target, Span<T> minSpan, string parameterName)
public static void MustBeSizedAtLeast<T>(ReadOnlySpan<T> target, ReadOnlySpan<T> minSpan, string parameterName)
where T : struct
{
if (target.Length < minSpan.Length)

8
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
/// <summary>
/// Fast division with ceiling for <see cref="uint"/> numbers.
/// </summary>
/// <param name="value">Divident value.</param>
/// <param name="divisor">Divisor value.</param>
/// <returns>Ceiled division result.</returns>
public static uint DivideCeil(uint value, uint divisor) => (value + divisor - 1) / divisor;
}
}

2
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
/// <see cref="IsNeedingInput"/> or <see cref="IsFinished"/> returns true or length is zero.
/// </returns>
public int Deflate(byte[] output, int offset, int length)
public int Deflate(Span<byte> output, int offset, int length)
{
int origLength = length;

41
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.
/// </summary>
private IManagedByteBuffer windowMemoryOwner;
private IMemoryOwner<byte> windowMemoryOwner;
private MemoryHandle windowMemoryHandle;
private readonly byte[] window;
private readonly Memory<byte> 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<byte>(2 * DeflaterConstants.WSIZE);
this.window = this.windowMemoryOwner.Memory;
this.windowMemoryHandle = this.window.Pin();
this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer;
this.headMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.HASH_SIZE);
this.head = this.headMemoryOwner.Memory;
this.headMemoryHandle = this.headMemoryOwner.Memory.Pin();
this.headMemoryHandle = this.head.Pin();
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer;
this.prevMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.WSIZE);
this.prev = this.prevMemoryOwner.Memory;
this.prevMemoryHandle = this.prevMemoryOwner.Memory.Pin();
this.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;
}

28
src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs

@ -41,11 +41,11 @@ namespace SixLabors.ImageSharp.Compression.Zlib
private Tree blTree;
// Buffer for distances
private readonly IMemoryOwner<short> distanceManagedBuffer;
private readonly IMemoryOwner<short> distanceMemoryOwner;
private readonly short* pinnedDistanceBuffer;
private MemoryHandle distanceBufferHandle;
private readonly IMemoryOwner<short> literalManagedBuffer;
private readonly IMemoryOwner<short> 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<short>(BufferSize);
this.distanceBufferHandle = this.distanceManagedBuffer.Memory.Pin();
this.distanceMemoryOwner = memoryAllocator.Allocate<short>(BufferSize);
this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin();
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer;
this.literalManagedBuffer = memoryAllocator.Allocate<short>(BufferSize);
this.literalBufferHandle = this.literalManagedBuffer.Memory.Pin();
this.literalMemoryOwner = memoryAllocator.Allocate<short>(BufferSize);
this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin();
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
@ -239,7 +239,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// <param name="storedLength">Count of bytes to write</param>
/// <param name="lastBlock">True if this is the last block</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock)
public void FlushStoredBlock(ReadOnlySpan<byte> 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
/// <param name="storedOffset">Index of first byte to flush</param>
/// <param name="storedLength">Count of bytes to flush</param>
/// <param name="lastBlock">True if this is the last block</param>
public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock)
public void FlushBlock(ReadOnlySpan<byte> 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<byte>(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<byte>(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<short> frequenciesMemoryOwner;
private MemoryHandle frequenciesMemoryHandle;
private IManagedByteBuffer lengthsMemoryOwner;
private IMemoryOwner<byte> 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<byte>(elements);
this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin();
this.Length = (byte*)this.lengthsMemoryHandle.Pointer;

27
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<byte> memoryOwner;
private readonly Memory<byte> 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<byte>(BufferLength);
this.buffer = this.memoryOwner.Memory;
this.deflater = new Deflater(memoryAllocator, compressionLevel);
}
@ -49,15 +50,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// <inheritdoc/>
public override long Position
{
get
{
return this.rawStream.Position;
}
get => this.rawStream.Position;
set
{
throw new NotSupportedException();
}
set => throw new NotSupportedException();
}
/// <inheritdoc/>
@ -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)

31
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
/// </summary>
internal sealed unsafe class DeflaterPendingBuffer : IDisposable
{
private readonly byte[] buffer;
private readonly Memory<byte> buffer;
private readonly byte* pinnedBuffer;
private IManagedByteBuffer bufferMemoryOwner;
private IMemoryOwner<byte> bufferMemoryOwner;
private MemoryHandle bufferMemoryHandle;
private int start;
@ -29,9 +30,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
public DeflaterPendingBuffer(MemoryAllocator memoryAllocator)
{
this.bufferMemoryOwner = memoryAllocator.AllocateManagedByteBuffer(DeflaterConstants.PENDING_BUF_SIZE);
this.buffer = this.bufferMemoryOwner.Array;
this.bufferMemoryHandle = this.bufferMemoryOwner.Memory.Pin();
this.bufferMemoryOwner = memoryAllocator.Allocate<byte>(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
/// <param name="offset">The offset of first byte to write.</param>
/// <param name="length">The number of bytes to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteBlock(byte[] block, int offset, int length)
public void WriteBlock(ReadOnlySpan<byte> 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
/// <param name="offset">The offset into output array.</param>
/// <param name="length">The maximum number of bytes to store.</param>
/// <returns>The number of bytes flushed.</returns>
public int Flush(byte[] output, int offset, int length)
public int Flush(Span<byte> 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;
}

23
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -348,17 +348,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
where TPixel : unmanaged, IPixel<TPixel>
{
bool isL8 = typeof(TPixel) == typeof(L8);
using (IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean))
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
if (isL8)
{
Span<byte> 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<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
@ -486,7 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
MaxColors = 2
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;

206
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -16,29 +16,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
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;
/// <summary>
/// <see cref="JpegFrame"/> instance containing decoding-related information.
/// </summary>
private JpegFrame frame;
// The spectral selection end.
private readonly int spectralEnd;
/// <summary>
/// Shortcut for <see cref="frame"/>.Components.
/// </summary>
private JpegComponent[] components;
// The successive approximation high bit end.
private readonly int successiveHigh;
/// <summary>
/// Number of component in the current scan.
/// </summary>
private int componentsCount;
// The successive approximation low bit end.
private readonly int successiveLow;
/// <summary>
/// The reset interval determined by RST markers.
/// </summary>
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;
/// <summary>
/// The DC Huffman tables.
/// </summary>
private readonly HuffmanTable[] dcHuffmanTables;
/// <summary>
/// The AC Huffman tables
/// </summary>
private readonly HuffmanTable[] acHuffmanTables;
// The unzig data.
private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer;
private readonly SpectralConverter spectralConverter;
private CancellationToken cancellationToken;
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="frame">The image frame.</param>
/// <param name="dcHuffmanTables">The DC Huffman tables.</param>
/// <param name="acHuffmanTables">The AC Huffman tables.</param>
/// <param name="componentsLength">The length of the components. Different to the array length.</param>
/// <param name="restartInterval">The reset interval.</param>
/// <param name="spectralStart">The spectral selection start.</param>
/// <param name="spectralEnd">The spectral selection end.</param>
/// <param name="successiveHigh">The successive approximation bit high end.</param>
/// <param name="successiveLow">The successive approximation bit low end.</param>
/// <param name="converter">Spectral to pixel converter.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
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];
}
/// <summary>
/// Sets reset interval determined by RST markers.
/// </summary>
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; }
/// <summary>
/// Decodes the entropy coded data.
/// </summary>
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<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(blockRow);
Span<Block8x8> 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<Block8x8, short>(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<Block8x8, short>(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;
}
/// <summary>
/// Build the huffman table using code lengths and code values.
/// </summary>
/// <param name="type">Table type.</param>
/// <param name="index">Table index.</param>
/// <param name="codeLengths">Code lengths.</param>
/// <param name="values">Code values.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void BuildHuffmanTable(int type, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
{
HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
tables[index] = new HuffmanTable(codeLengths, values);
}
}
}

17
src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs

@ -11,26 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
internal interface IRawJpegData : IDisposable
{
/// <summary>
/// Gets the image size in pixels.
/// </summary>
Size ImageSizeInPixels { get; }
/// <summary>
/// Gets the number of components.
/// </summary>
int ComponentCount { get; }
/// <summary>
/// Gets the color space
/// </summary>
JpegColorSpace ColorSpace { get; }
/// <summary>
/// Gets the number of bits used for precision.
/// </summary>
int Precision { get; }
/// <summary>
/// Gets the components.
/// </summary>
@ -41,4 +26,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
Block8x8F[] QuantizationTables { get; }
}
}
}

6
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs

@ -38,11 +38,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
private Size subSamplingDivisors;
/// <summary>
/// Defines the maximum value derived from the bitdepth.
/// </summary>
private readonly int maximumValue;
/// <summary>
/// Initializes a new instance of the <see cref="JpegBlockPostProcessor"/> struct.
/// </summary>
@ -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;

30
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()
/// <summary>
/// Initializes component for future buffers initialization.
/// </summary>
/// <param name="maxSubFactorH">Maximal horizontal subsampling factor among all the components.</param>
/// <param name="maxSubFactorV">Maximal vertical subsampling factor among all the components.</param>
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<Block8x8>(width, height, AllocationOptions.Clean);
this.SpectralBlocks = this.memoryAllocator.Allocate2D<Block8x8>(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean);
}
}
}

64
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
{
/// <summary>
/// Encapsulates postprocessing data for one component for <see cref="JpegImagePostProcessor"/>.
/// Encapsulates spectral data to rgba32 processing for one component.
/// </summary>
internal class JpegComponentPostProcessor : IDisposable
{
@ -24,26 +21,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
private readonly Size blockAreaSize;
/// <summary>
/// Jpeg frame instance containing required decoding metadata.
/// </summary>
private readonly JpegFrame frame;
/// <summary>
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor"/> class.
/// </summary>
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<float>(
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;
}
/// <summary>
/// Gets the <see cref="JpegImagePostProcessor"/>
/// </summary>
public JpegImagePostProcessor ImagePostProcessor { get; }
public IRawJpegData RawJpeg { get; }
/// <summary>
/// Gets the <see cref="Component"/>
@ -66,26 +67,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public int BlockRowsPerStep { get; }
/// <inheritdoc />
public void Dispose()
{
this.ColorBuffer.Dispose();
}
public void Dispose() => this.ColorBuffer.Dispose();
/// <summary>
/// Invoke <see cref="JpegBlockPostProcessor"/> for <see cref="BlockRowsPerStep"/> block rows, copy the result into <see cref="ColorBuffer"/>.
/// </summary>
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<Block8x8> 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<float> colorBufferRow = this.ColorBuffer.GetRowSpan(yBuffer);
Span<Block8x8> blockRow = this.Component.SpectralBlocks.GetRowSpan(yBlock);
Span<Block8x8> 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<Block8x8> 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;
}
}

91
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs

@ -10,35 +10,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
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;
}
/// <summary>
/// Gets a value indicating whether the frame uses the extended specification.
/// </summary>
public bool Extended { get; private set; }
/// <summary>
/// Gets a value indicating whether the frame uses the progressive specification.
/// </summary>
public bool Progressive { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the frame is encoded using multiple scans (SOS markers).
/// </summary>
/// <remarks>
/// This is true for progressive and baseline non-interleaved images.
/// </remarks>
public bool MultiScan { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame uses the extended specification.
/// Gets the precision.
/// </summary>
public bool Extended { get; set; }
public byte Precision { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the frame uses the progressive specification.
/// Gets the maximum color value derived from <see cref="Precision"/>.
/// </summary>
public bool Progressive { get; set; }
public float MaxColorChannelValue { get; private set; }
/// <summary>
/// Gets or sets the precision.
/// Gets the number of pixel per row.
/// </summary>
public byte Precision { get; set; }
public int PixelHeight { get; private set; }
/// <summary>
/// Gets or sets the number of scanlines within the frame.
/// Gets the number of pixels per line.
/// </summary>
public int Scanlines { get; set; }
public int PixelWidth { get; private set; }
/// <summary>
/// Gets or sets the number of samples per scanline.
/// Gets the pixel size of the image.
/// </summary>
public int SamplesPerLine { get; set; }
public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight);
/// <summary>
/// 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.
/// </summary>
public byte ComponentCount { get; set; }
public byte ComponentCount { get; private set; }
/// <summary>
/// Gets or sets the component id collection.
@ -57,24 +89,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public JpegComponent[] Components { get; set; }
/// <summary>
/// Gets or sets the maximum horizontal sampling factor.
/// Gets or sets the number of MCU's per line.
/// </summary>
public int MaxHorizontalFactor { get; set; }
public int McusPerLine { get; set; }
/// <summary>
/// Gets or sets the maximum vertical sampling factor.
/// Gets or sets the number of MCU's per column.
/// </summary>
public int MaxVerticalFactor { get; set; }
public int McusPerColumn { get; set; }
/// <summary>
/// Gets or sets the number of MCU's per line.
/// Gets the mcu size of the image.
/// </summary>
public int McusPerLine { get; set; }
public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn);
/// <summary>
/// Gets or sets the number of MCU's per column.
/// Gets the color depth, in number of bits per pixel.
/// </summary>
public int McusPerColumn { get; set; }
public int BitsPerPixel => this.ComponentCount * this.Precision;
/// <inheritdoc/>
public void Dispose()
@ -93,15 +125,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Allocates the frame component blocks.
/// </summary>
public void InitComponents()
/// <param name="maxSubFactorH">Maximal horizontal subsampling factor among all the components.</param>
/// <param name="maxSubFactorV">Maximal vertical subsampling factor among all the components.</param>
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);
}
}
}

181
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs

@ -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
{
/// <summary>
/// Encapsulates the execution od post-processing algorithms to be applied on a <see cref="IRawJpegData"/> to produce a valid <see cref="Image{TPixel}"/>: <br/>
/// (1) Dequantization <br/>
/// (2) IDCT <br/>
/// (3) Color conversion form one of the <see cref="JpegColorSpace"/>-s into a <see cref="Vector4"/> buffer of RGBA values <br/>
/// (4) Packing <see cref="Image{TPixel}"/> pixels from the <see cref="Vector4"/> buffer. <br/>
/// These operations are executed in <see cref="NumberOfPostProcessorSteps"/> steps.
/// <see cref="PixelRowsPerStep"/> image rows are converted in one step,
/// which means that size of the allocated memory is limited (does not depend on <see cref="ImageFrame.Height"/>).
/// </summary>
internal class JpegImagePostProcessor : IDisposable
{
private readonly Configuration configuration;
/// <summary>
/// The number of block rows to be processed in one Step.
/// </summary>
public const int BlockRowsPerStep = 4;
/// <summary>
/// The number of image pixel rows to be processed in one step.
/// </summary>
public const int PixelRowsPerStep = 4 * 8;
/// <summary>
/// Temporal buffer to store a row of colors.
/// </summary>
private readonly IMemoryOwner<Vector4> rgbaBuffer;
/// <summary>
/// The <see cref="JpegColorConverter"/> corresponding to the current <see cref="JpegColorSpace"/> determined by <see cref="IRawJpegData.ColorSpace"/>.
/// </summary>
private readonly JpegColorConverter colorConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JpegImagePostProcessor"/> class.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
/// <param name="rawJpeg">The <see cref="IRawJpegData"/> representing the uncompressed spectral Jpeg data</param>
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<Vector4>(rawJpeg.ImageSizeInPixels.Width);
this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision);
}
/// <summary>
/// Gets the <see cref="JpegComponentPostProcessor"/> instances.
/// </summary>
public JpegComponentPostProcessor[] ComponentProcessors { get; }
/// <summary>
/// Gets the <see cref="IRawJpegData"/> to be processed.
/// </summary>
public IRawJpegData RawJpeg { get; }
/// <summary>
/// Gets the total number of post processor steps deduced from the height of the image and <see cref="PixelRowsPerStep"/>.
/// </summary>
public int NumberOfPostProcessorSteps { get; }
/// <summary>
/// Gets the size of the temporary buffers we need to allocate into <see cref="JpegComponentPostProcessor.ColorBuffer"/>.
/// </summary>
public Size PostProcessorBufferSize { get; }
/// <summary>
/// Gets the value of the counter that grows by each step by <see cref="PixelRowsPerStep"/>.
/// </summary>
public int PixelRowCounter { get; private set; }
/// <inheritdoc />
public void Dispose()
{
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors)
{
cpp.Dispose();
}
this.rgbaBuffer.Dispose();
}
/// <summary>
/// Process all pixels into 'destination'. The image dimensions should match <see cref="RawJpeg"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="destination">The destination image</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void PostProcess<TPixel>(ImageFrame<TPixel> destination, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
/// <summary>
/// Execute one step processing <see cref="PixelRowsPerStep"/> pixel rows into 'destination'.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="destination">The destination image.</param>
public void DoPostProcessorStep<TPixel>(ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors)
{
cpp.CopyBlocksToColorBuffer();
}
this.ConvertColorsInto(destination);
this.PixelRowCounter += PixelRowsPerStep;
}
/// <summary>
/// Convert and copy <see cref="PixelRowsPerStep"/> row of colors into 'destination' starting at row <see cref="PixelRowCounter"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="destination">The destination image</param>
private void ConvertColorsInto<TPixel>(ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep);
var buffers = new Buffer2D<float>[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<TPixel> destRow = destination.GetPixelRowSpan(yy);
// TODO: Investigate if slicing is actually necessary
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow);
}
}
}
}

34
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
{
/// <summary>
/// Converter used to convert jpeg spectral data.
/// </summary>
/// <remarks>
/// This is tightly coupled with <see cref="HuffmanScanDecoder"/> and <see cref="JpegDecoderCore"/>.
/// </remarks>
internal abstract class SpectralConverter
{
/// <summary>
/// Injects jpeg image decoding metadata.
/// </summary>
/// <remarks>
/// This is guaranteed to be called only once at SOF marker by <see cref="HuffmanScanDecoder"/>.
/// </remarks>
/// <param name="frame"><see cref="JpegFrame"/> instance containing decoder-specific parameters.</param>
/// <param name="jpegData"><see cref="IRawJpegData"/> instance containing decoder-specific parameters.</param>
public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData);
/// <summary>
/// Called once per spectral stride for each component in <see cref="HuffmanScanDecoder"/>.
/// This is called only for baseline interleaved jpegs.
/// </summary>
/// <remarks>
/// Spectral 'stride' doesn't particularly mean 'single stride'.
/// Actual stride height depends on the subsampling factor of the given component.
/// </remarks>
public abstract void ConvertStrideBaseline();
}
}

146
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<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Configuration configuration;
private CancellationToken cancellationToken;
private JpegComponentPostProcessor[] componentProcessors;
private JpegColorConverter colorConverter;
private IMemoryOwner<Vector4> rgbaBuffer;
private Buffer2D<TPixel> 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<TPixel> 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<TPixel>(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<Vector4>(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<float>[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<TPixel> destRow = this.pixelBuffer.GetRowSpan(yy);
// TODO: Investigate if slicing is actually necessary
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow);
}
this.pixelRowCounter += this.pixelRowsPerStep;
}
}
}

4
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<TPixel> source, Span<TPixel> 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);

335
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
/// <summary>
/// The only supported precision
/// </summary>
private readonly int[] supportedPrecisions = { 8, 12 };
private readonly byte[] supportedPrecisions = { 8, 12 };
/// <summary>
/// The buffer used to temporarily store bytes read from the stream.
@ -41,21 +42,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly byte[] markerBuffer = new byte[2];
/// <summary>
/// The DC Huffman tables.
/// </summary>
private HuffmanTable[] dcHuffmanTables;
/// <summary>
/// The AC Huffman tables
/// </summary>
private HuffmanTable[] acHuffmanTables;
/// <summary>
/// The reset interval determined by RST markers.
/// </summary>
private ushort resetInterval;
/// <summary>
/// Whether the image has an EXIF marker.
/// </summary>
@ -96,6 +82,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private AdobeMarker adobe;
/// <summary>
/// Scan decoder.
/// </summary>
private HuffmanScanDecoder scanDecoder;
/// <summary>
/// Initializes a new instance of the <see cref="JpegDecoderCore" /> class.
/// </summary>
@ -116,30 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public JpegFrame Frame { get; private set; }
/// <inheritdoc/>
public Size ImageSizeInPixels { get; private set; }
/// <inheritdoc/>
Size IImageDecoderInternals.Dimensions => this.ImageSizeInPixels;
/// <summary>
/// Gets the number of MCU blocks in the image as <see cref="Size"/>.
/// </summary>
public Size ImageSizeInMCU { get; private set; }
/// <summary>
/// Gets the image width
/// </summary>
public int ImageWidth => this.ImageSizeInPixels.Width;
/// <summary>
/// Gets the image height
/// </summary>
public int ImageHeight => this.ImageSizeInPixels.Height;
/// <summary>
/// Gets the color depth, in number of bits per pixel.
/// </summary>
public int BitsPerPixel => this.ComponentCount * this.Frame.Precision;
Size IImageDecoderInternals.Dimensions => this.Frame.PixelSize;
/// <summary>
/// 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
/// </summary>
public ImageMetadata Metadata { get; private set; }
/// <inheritdoc/>
public int ComponentCount { get; private set; }
/// <inheritdoc/>
public JpegColorSpace ColorSpace { get; private set; }
/// <inheritdoc/>
public int Precision { get; private set; }
/// <summary>
/// Gets the components.
/// </summary>
@ -212,34 +174,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ParseStream(stream, cancellationToken: cancellationToken);
using var spectralConverter = new SpectralConverter<TPixel>(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<TPixel>(cancellationToken);
return new Image<TPixel>(this.Configuration, spectralConverter.PixelBuffer, this.Metadata);
}
/// <inheritdoc/>
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);
}
/// <summary>
/// Parses the input stream for file markers
/// Parses the input stream for file markers.
/// </summary>
/// <param name="stream">The input stream</param>
/// <param name="metadataOnly">Whether to decode metadata only.</param>
/// <param name="stream">The input stream.</param>
/// <param name="scanDecoder">Scan decoder used exclusively to decode SOS marker.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
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;
}
/// <summary>
/// Returns the correct colorspace based on the image component count
/// </summary>
/// <returns>The <see cref="JpegColorSpace"/></returns>
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<byte> blockDataSpan = resourceBlockData.AsSpan();
@ -644,8 +607,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
Span<byte> 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<byte> 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
/// <returns>The block length.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadResourceDataLength(Span<byte> blockDataSpan, int resourceBlockNameLength)
{
return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
}
=> BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
/// <summary>
/// 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<byte> huffmanData = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean))
{
ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanData.GetSpan());
Span<byte> 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<byte> codeLengths = this.Configuration.MemoryAllocator.Allocate<byte>(17, AllocationOptions.Clean))
{
ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengths.GetSpan());
Span<byte> 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<byte> huffmanValues = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean))
{
stream.Read(huffmanValues.Array, 0, codeLengthSum);
Span<byte> 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);
}
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
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);
}
/// <summary>
/// Builds the huffman tables
/// </summary>
/// <param name="tables">The tables</param>
/// <param name="index">The table index</param>
/// <param name="codeLengths">The codelengths</param>
/// <param name="values">The values</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void BuildHuffmanTable(HuffmanTable[] tables, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
=> tables[index] = new HuffmanTable(codeLengths, values);
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes
/// </summary>
@ -1083,32 +1069,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.markerBuffer, 0, 2);
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}
/// <summary>
/// Post processes the pixels into the destination image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
private Image<TPixel> PostProcessIntoImage<TPixel>(CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.ImageWidth == 0 || this.ImageHeight == 0)
{
JpegThrowHelper.ThrowInvalidImageDimensions(this.ImageWidth, this.ImageHeight);
}
var image = Image.CreateUninitialized<TPixel>(
this.Configuration,
this.ImageWidth,
this.ImageHeight,
this.Metadata);
using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this))
{
postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken);
}
return image;
}
}
}

4
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<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSameSized<byte>(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
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));

4
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<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSameSized<byte>(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
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));

2
src/ImageSharp/Formats/Png/Filters/SubFilter.cs

@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(Span<byte> scanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));

4
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<byte> scanline, Span<byte> previousScanline)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSameSized<byte>(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
/// <param name="result">The filtered scanline result.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, out int sum)
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));

6
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
/// </summary>
internal readonly struct PngChunk
{
public PngChunk(int length, PngChunkType type, IManagedByteBuffer data = null)
public PngChunk(int length, PngChunkType type, IMemoryOwner<byte> 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.
/// </summary>
public IManagedByteBuffer Data { get; }
public IMemoryOwner<byte> Data { get; }
/// <summary>
/// Gets a value indicating whether the given chunk is critical to decoding

327
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
/// <summary>
/// Previous scanline processed.
/// </summary>
private IManagedByteBuffer previousScanline;
private IMemoryOwner<byte> previousScanline;
/// <summary>
/// The current scanline that is being processed.
/// </summary>
private IManagedByteBuffer scanline;
private IMemoryOwner<byte> scanline;
/// <summary>
/// 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
/// <param name="bits">The number of bits per value.</param>
/// <param name="buffer">The new array.</param>
/// <returns>The resulting <see cref="ReadOnlySpan{Byte}"/> array.</returns>
private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, out IManagedByteBuffer buffer)
private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, out IMemoryOwner<byte> 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<byte>(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<byte>(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.Configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
}
/// <summary>
@ -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<byte> 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<byte> 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<TPixel> 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<byte> trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline - 1, this.header.BitDepth, out IManagedByteBuffer buffer)
? buffer.GetSpan()
: trimmed;
switch (this.pngColorType)
IMemoryOwner<byte> buffer = null;
try
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessGrayscaleScanline(
this.header,
scanlineSpan,
rowSpan,
pngMetadata.HasTransparency,
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
ReadOnlySpan<byte> 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();
}
/// <summary>
@ -735,78 +754,88 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan<byte> trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline, this.header.BitDepth, out IManagedByteBuffer buffer)
? buffer.GetSpan()
: trimmed;
switch (this.pngColorType)
IMemoryOwner<byte> buffer = null;
try
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
pngMetadata.HasTransparency,
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
ReadOnlySpan<byte> 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();
}
/// <summary>
@ -1189,12 +1218,12 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <param name="length">The length of the chunk data to read.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private IManagedByteBuffer ReadChunkData(int length)
private IMemoryOwner<byte> ReadChunkData(int length)
{
// We rent the buffer here to return it afterwards in Decode()
IManagedByteBuffer buffer = this.Configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean);
IMemoryOwner<byte> buffer = this.Configuration.MemoryAllocator.Allocate<byte>(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<byte> temp = this.previousScanline;
this.previousScanline = this.scanline;
this.scanline = temp;
}

353
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -80,32 +80,12 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// The raw data of previous scanline.
/// </summary>
private IManagedByteBuffer previousScanline;
private IMemoryOwner<byte> previousScanline;
/// <summary>
/// The raw data of current scanline.
/// </summary>
private IManagedByteBuffer currentScanline;
/// <summary>
/// The common buffer for the filters.
/// </summary>
private IManagedByteBuffer filterBuffer;
/// <summary>
/// The ext buffer for the sub filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer subFilter;
/// <summary>
/// The ext buffer for the average filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer averageFilter;
/// <summary>
/// The ext buffer for the Paeth filter, <see cref="PngFilterMethod.Adaptive"/>.
/// </summary>
private IManagedByteBuffer paethFilter;
private IMemoryOwner<byte> currentScanline;
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> 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;
}
/// <summary>
@ -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<byte> tempSpan = temp.GetSpan();
// We need to first create an array of luminance bytes then scale them down to the correct bit depth.
PixelOperations<TPixel>.Instance.ToL8Bytes(
this.configuration,
rowSpan,
tempSpan,
rowSpan.Length);
PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
}
using IMemoryOwner<byte> temp = this.memoryAllocator.Allocate<byte>(rowSpan.Length, AllocationOptions.Clean);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(this.bitDepth) - 1);
Span<byte> tempSpan = temp.GetSpan();
// We need to first create an array of luminance bytes then scale them down to the correct bit depth.
PixelOperations<TPixel>.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
}
/// <summary>
/// Apply filter for the raw scanline.
/// Apply the line filter for the raw scanline to enable better compression.
/// </summary>
private IManagedByteBuffer FilterPixelBytes()
private void FilterPixelBytes(ref Span<byte> filter, ref Span<byte> 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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
/// <param name="filter">The filtered buffer.</param>
/// <param name="attempt">Used for attempting optimized filtering.</param>
/// <param name="quantized">The quantized pixels. Can be <see langword="null"/>.</param>
/// <param name="row">The row number.</param>
private void CollectAndFilterPixelRow<TPixel>(
ReadOnlySpan<TPixel> rowSpan,
ref Span<byte> filter,
ref Span<byte> attempt,
IndexedImageFrame<TPixel> quantized,
int row)
where TPixel : unmanaged, IPixel<TPixel>
{
this.CollectPixelBytes(rowSpan, quantized, row);
return this.FilterPixelBytes();
this.FilterPixelBytes(ref filter, ref attempt);
}
/// <summary>
/// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode.
/// </summary>
/// <param name="rowSpan">The row span.</param>
private IManagedByteBuffer EncodeAdam7IndexedPixelRow(ReadOnlySpan<byte> rowSpan)
/// <param name="row">The row span.</param>
/// <param name="filter">The filtered buffer.</param>
/// <param name="attempt">Used for attempting optimized filtering.</param>
private void EncodeAdam7IndexedPixelRow(
ReadOnlySpan<byte> row,
ref Span<byte> filter,
ref Span<byte> 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);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="T:byte[]"/></returns>
private IManagedByteBuffer GetOptimalFilteredScanline()
private void ApplyOptimalFilteredScanline(ref Span<byte> filter, ref Span<byte> 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<byte> scanSpan = this.currentScanline.GetSpan();
Span<byte> 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<byte> current = this.currentScanline.GetSpan();
Span<byte> 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);
}
}
/// <summary>
@ -612,8 +584,8 @@ namespace SixLabors.ImageSharp.Formats.Png
int colorTableLength = paletteLength * Unsafe.SizeOf<Rgb24>();
bool hasAlpha = false;
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
using IMemoryOwner<byte> colorTable = this.memoryAllocator.Allocate<byte>(colorTableLength);
using IMemoryOwner<byte> alphaTable = this.memoryAllocator.Allocate<byte>(paletteLength);
ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(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.
/// </summary>
/// <param name="bytesPerScanline">The bytes per scanline.</param>
/// <param name="resultLength">Length of the result.</param>
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);
}
/// <summary>
/// Allocates the ext buffers for adaptive filter.
/// </summary>
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<byte>(bytesPerScanline, AllocationOptions.Clean);
this.currentScanline = this.memoryAllocator.Allocate<byte>(bytesPerScanline, AllocationOptions.Clean);
}
/// <summary>
@ -969,17 +916,19 @@ namespace SixLabors.ImageSharp.Formats.Png
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = bytesPerScanline + 1;
this.AllocateBuffers(bytesPerScanline, resultLength);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using IMemoryOwner<byte> attemptBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
Span<byte> filter = filterBuffer.GetSpan();
Span<byte> 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<TPixel> blockBuffer = this.memoryAllocator.Allocate<TPixel>(blockWidth);
using IMemoryOwner<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using IMemoryOwner<byte> attemptBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using (IMemoryOwner<TPixel> passData = this.memoryAllocator.Allocate<TPixel>(blockWidth))
Span<TPixel> block = blockBuffer.GetSpan();
Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan();
for (int row = startRow; row < height; row += Adam7.RowIncrement[pass])
{
Span<TPixel> destSpan = passData.Memory.Span;
for (int row = startRow;
row < height;
row += Adam7.RowIncrement[pass])
// Collect pixel data
Span<TPixel> srcRow = pixels.GetPixelRowSpan(row);
for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass])
{
// collect data
Span<TPixel> 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<TPixel>)destSpan, null, -1);
deflateStream.Write(r.Array, 0, resultLength);
// Encode data
// Note: quantized parameter not used
// Note: row parameter not used
this.CollectAndFilterPixelRow<TPixel>(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<byte> passData = this.memoryAllocator.Allocate<byte>(blockWidth))
using IMemoryOwner<byte> blockBuffer = this.memoryAllocator.Allocate<byte>(blockWidth);
using IMemoryOwner<byte> filterBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
using IMemoryOwner<byte> attemptBuffer = this.memoryAllocator.Allocate<byte>(filterLength, AllocationOptions.Clean);
Span<byte> block = blockBuffer.GetSpan();
Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan();
for (int row = startRow;
row < height;
row += Adam7.RowIncrement[pass])
{
Span<byte> destSpan = passData.Memory.Span;
for (int row = startRow;
row < height;
row += Adam7.RowIncrement[pass])
// Collect data
ReadOnlySpan<byte> srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0;
col < width;
col += Adam7.ColumnIncrement[pass])
{
// collect data
ReadOnlySpan<byte> 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
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
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<byte> data)
=> this.WriteChunk(stream, type, data, 0, data.Length);
/// <summary>
/// Writes a chunk of a specified length to the stream at the given offset.
@ -1113,7 +1062,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param>
private void WriteChunk(Stream stream, PngChunkType type, byte[] data, int offset, int length)
private void WriteChunk(Stream stream, PngChunkType type, Span<byte> 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<byte> temp = this.previousScanline;
this.previousScanline = this.currentScanline;
this.currentScanline = temp;
}
private static void SwapSpans<T>(ref Span<T> a, ref Span<T> b)
{
Span<T> t = b;
b = a;
a = t;
}
}
}

13
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)

58
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
{
/// <summary>
/// Implements the 'RGB' photometric interpretation with 16 bits for each channel.
/// </summary>
internal class Rgb161616TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly bool isBigEndian;
/// <summary>
/// Initializes a new instance of the <see cref="Rgb161616TiffColor{TPixel}" /> class.
/// </summary>
/// <param name="isBigEndian">if set to <c>true</c> decodes the pixel data as big endian, otherwise as little endian.</param>
public Rgb161616TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian;
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> 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<TPixel> 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<byte> buffer) => this.isBigEndian
? BinaryPrimitives.ReadUInt16BigEndian(buffer)
: BinaryPrimitives.ReadUInt16LittleEndian(buffer);
}
}

4
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
internal static class TiffColorDecoderFactory<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
public static TiffBaseColorDecoder<TPixel> Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap)
public static TiffBaseColorDecoder<TPixel> 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<TPixel>(bitsPerSample);
return new Rgb161616TiffColor<TPixel>(isBigEndian: byteOrder == ByteOrder.BigEndian);
case TiffColorType.PaletteColor:
DebugGuard.NotNull(colorMap, "colorMap");

8
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -36,6 +36,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
private BufferedReadStream inputStream;
/// <summary>
/// Indicates the byte order of the stream.
/// </summary>
private ByteOrder byteOrder;
/// <summary>
/// Initializes a new instance of the <see cref="TiffDecoderCore" /> class.
/// </summary>
@ -109,6 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var reader = new DirectoryReader(stream);
IEnumerable<ExifProfile> directories = reader.Read();
this.byteOrder = reader.ByteOrder;
var frames = new List<ImageFrame<TPixel>>();
foreach (ExifProfile ifd in directories)
@ -310,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.Predictor,
this.FaxCompressionOptions);
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample, this.ColorMap);
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder);
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{

2
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<byte>(bytesPerStrip);
this.pixelsAsGray ??= this.MemoryAllocator.Allocate<byte>(width);
Span<byte> pixelAsGraySpan = this.pixelsAsGray.GetSpan();

4
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<byte>(stripPixels);
Span<byte> 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<byte> colorPaletteBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(this.colorPaletteBytes);
using IMemoryOwner<byte> colorPaletteBuffer = this.MemoryAllocator.Allocate<byte>(this.colorPaletteBytes);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColors = this.quantizedImage.Palette.Span;

15
src/ImageSharp/Image{TPixel}.cs

@ -87,6 +87,21 @@ namespace SixLabors.ImageSharp
this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// wrapping an external <see cref="Buffer2D{TPixel}"/> pixel bufferx.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="pixelBuffer">Pixel buffer.</param>
/// <param name="metadata">The images metadata.</param>
internal Image(
Configuration configuration,
Buffer2D<TPixel> pixelBuffer,
ImageMetadata metadata)
: this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// wrapping an external <see cref="MemoryGroup{T}"/>.

4
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();
/// <summary>
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="System.Memory{T}"/> of length <paramref name="length"/>.
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="length">Size of the buffer to allocate.</param>

18
tests/Directory.Build.targets

@ -18,17 +18,21 @@
<ItemGroup>
<!-- Test Dependencies -->
<PackageReference Update="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.1" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="2.0.5" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.23.2.1" />
<PackageReference Update="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="3.0.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="8.0.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Moq" Version="4.14.6" />
<PackageReference Update="NetVips" Version="2.0.1" />
<PackageReference Update="NetVips.Native" Version="8.11.0" />
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.12.1" />
<PackageReference Update="Pfim" Version="0.9.1" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.3.0" />
<PackageReference Update="System.Drawing.Common" Version="4.7.0" />
<PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.3.2" />
<PackageReference Update="SkiaSharp" Version="2.80.2" />
<PackageReference Update="System.Drawing.Common" Version="5.0.2" />
</ItemGroup>
</Project>

54
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 |
*/

6
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<XYZColor, LabColor> 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")]

6
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<XYZColor, HunterLabColor> 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")]

5
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<XYZColor, LMSColor> 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")]

5
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<XYZColor, RGBColor> 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")]

9
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<RGBColor, RGBColor> 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);
}

7
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -9,7 +9,7 @@
<!--Used to hide test project from dotnet test-->
<IsTestProject>false</IsTestProject>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<LangVersion>9</LangVersion>
<!-- Uncomment this to run benchmarks depending on Colorful or Pfim (colorspaces and TGA): -->
<!--<SignAssembly>false</SignAssembly>-->
</PropertyGroup>
@ -38,8 +38,13 @@
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Colourful" />
<PackageReference Include="NetVips"/>
<PackageReference Include="NetVips.Native" />
<PackageReference Include="PhotoSauce.MagicScaler"/>
<PackageReference Include="Pfim" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>

69
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<string> 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);
}
}

280
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<string> 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);
}
}
}

9
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 `<solution-dir>\tests\Images\ActualOutput\MemoryStress\`.

7
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -14,6 +14,7 @@
<EnsureNETCoreAppRuntime>false</EnsureNETCoreAppRuntime>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<LangVersion>9</LangVersion>
</PropertyGroup>
<Choose>
@ -31,11 +32,17 @@
<ItemGroup>
<Compile Remove="..\ImageSharp.Tests\IO\LocalFileSystem.cs" />
<Compile Include="..\ImageSharp.Benchmarks\LoadResizeSave\LoadResizeSaveStressRunner.cs" Link="LoadResizeSaveStressRunner.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" />
<PackageReference Include="Moq" />
<PackageReference Include="NetVips" />
<PackageReference Include="NetVips.Native" />
<PackageReference Include="PhotoSauce.MagicScaler"/>
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>

142
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<string> 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);
}
}

5
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -31,13 +31,14 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
/// </param>
public static void Main(string[] args)
{
RunJpegEncoderProfilingTests();
LoadResizeSaveParallelMemoryStress.Run();
// RunJpegEncoderProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest();
// RunResizeProfilingTest();
Console.ReadLine();
// Console.ReadLine();
}
private static void RunJpegEncoderProfilingTests()

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

1
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
{

1
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
{

1
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
{

1
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
{

8
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<string, float> 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:

15
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<Rgba32> image = decoder.Decode<Rgba32>(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 =>
{

1
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
{

97
tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs

@ -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<TPixel>(JpegComponentPostProcessor cp, TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<Rgba32> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
string imageFile = provider.SourceFileOrDescription;
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder))
using (var imageFrame = new ImageFrame<Rgba32>(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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
string imageFile = provider.SourceFileOrDescription;
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile))
using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder))
using (var image = new Image<Rgba32>(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<TPixel> referenceImage =
provider.GetReferenceOutputImage<TPixel>(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);
}
}
}
}
}

14
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);

118
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>();
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<TPixel>();
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<TPixel>(
TestImageProvider<TPixel> provider,
private void VerifySpectralCorrectnessImpl(
LibJpegTools.SpectralData libJpegData,
LibJpegTools.SpectralData imageSharpData)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> : SpectralConverter
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>
foreach (JpegComponent component in this.frame.Components)
{
Buffer2D<Block8x8> 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);
}
}
}
}

69
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel>(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<TPixel> image = new Image<TPixel>(Configuration.Default, converter.PixelBuffer, new ImageMetadata()))
using (Image<TPixel> referenceImage = provider.GetReferenceOutputImage<TPixel>(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);
}
}
}
}

10
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<Rgba32> image = decoder.Decode<Rgba32>(bufferedStream, cancellationToken: default);
}
return decoder;
}

45
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<Block8x8> 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<Block8x8> blockRow = c.SpectralBlocks.GetRowSpan(y);
for (int x = 0; x < result.WidthInBlocks; x++)
Span<Block8x8> 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<Block8x8> data = c.SpectralBlocks;
for (int y = 0; y < this.HeightInBlocks; y++)
{
Span<Block8x8> 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;
}

8
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<Rgba32> TryCreateRGBSpectralImage()
{
if (this.ComponentCount != 3)

1
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
{

1
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
{

10
tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs

@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EncodePaethFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void EncodePaethFilter(ReadOnlySpan<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSameSized<byte>(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EncodeSubFilter(Span<byte> scanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void EncodeSubFilter(ReadOnlySpan<byte> scanline, Span<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// <param name="result">The filtered scanline result.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EncodeUpFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, out int sum)
public static void EncodeUpFilter(ReadOnlySpan<byte> scanline, Span<byte> previousScanline, Span<byte> result, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
/// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EncodeAverageFilter(Span<byte> scanline, Span<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
public static void EncodeAverageFilter(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));

1
tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs

@ -16,6 +16,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tga;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Tga
{
[Collection("RunSerial")]
[Trait("Format", "Tga")]
public class TgaDecoderTests
{

1
tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs

@ -13,6 +13,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tga;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Tga
{
[Collection("RunSerial")]
[Trait("Format", "Tga")]
public class TgaEncoderTests
{

25
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@ -242,19 +243,31 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[MemberData(nameof(Rgb4Data))]
[MemberData(nameof(Rgb8Data))]
[MemberData(nameof(Rgb484_Data))]
public void Decode_WritesPixelData(byte[][] inputData, TiffBitsPerSample bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
public void Decode_WritesPixelData(
byte[][] inputData,
TiffBitsPerSample bitsPerSample,
int left,
int top,
int width,
int height,
Rgba32[][] expectedResult)
=> AssertDecode(
expectedResult,
pixels =>
{
var buffers = new IManagedByteBuffer[inputData.Length];
var buffers = new IMemoryOwner<byte>[inputData.Length];
for (int i = 0; i < buffers.Length; i++)
{
buffers[i] = Configuration.Default.MemoryAllocator.AllocateManagedByteBuffer(inputData[i].Length);
buffers[i] = Configuration.Default.MemoryAllocator.Allocate<byte>(inputData[i].Length);
((Span<byte>)inputData[i]).CopyTo(buffers[i].GetSpan());
}
new RgbPlanarTiffColor<Rgba32>(bitsPerSample).Decode(buffers, pixels, left, top, width, height);
foreach (IMemoryOwner<byte> buffer in buffers)
{
buffer.Dispose();
}
});
}
}
}

2
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -17,6 +17,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
[Collection("RunSerial")]
[Trait("Format", "Tiff")]
public class TiffDecoderTests
{
@ -163,6 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb161616Planar, PixelTypes.Rgba32)]
[WithFile(Issues1716Rgb161616BitLittleEndian, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_48Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);

1
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -12,6 +12,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
[Collection("RunSerial")]
[Trait("Format", "Tiff")]
public class TiffEncoderTests : TiffEncoderBaseTester
{

1
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -39,6 +39,7 @@
<PackageReference Include="Microsoft.DotNet.RemoteExecutor" />
<PackageReference Include="Microsoft.DotNet.XUnitExtensions" />
<PackageReference Include="Moq" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>

3
tests/ImageSharp.Tests/TestImages.cs

@ -261,6 +261,8 @@ namespace SixLabors.ImageSharp.Tests
public const string AccessViolationException827 = "Jpg/issues/fuzz/Issue827-AccessViolationException.jpg";
public const string ExecutionEngineException839 = "Jpg/issues/fuzz/Issue839-ExecutionEngineException.jpg";
public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg";
public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg";
public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg";
}
}
@ -580,6 +582,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff";
public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff";
public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff";
public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";

4
tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a76832570111a868ea6cb6e8287aae1976c575c94c63880c74346a4b5db5d305
size 27007
oid sha256:2b5e1d91fb6dc1ddb696fbee63331ba9c6ef3548b619c005887e60c5b01f4981
size 27303

3
tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fbb6acd612cdb09825493d04ec7c6aba8ef2a94cc9a86c6b16218720adfb8f5c
size 58065

3
tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8720a9ccf118c3f55407aa250ee490d583286c7e40c8c62a6f8ca449ca3ddff3
size 58067

3
tests/Images/Input/Tiff/Issues/Issue1716.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c734dd489c65fb77bd7a35cd663aa16ce986df2c2ab8c7ca43d8b65db9d47c03
size 6666162
Loading…
Cancel
Save