Browse Source

Merge branch 'master' into webp

pull/1552/head
Brian Popow 5 years ago
committed by GitHub
parent
commit
79119aa9e7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/ImageSharp/Common/Helpers/Numerics.cs
  2. 206
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  3. 17
      src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
  4. 6
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs
  5. 30
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
  6. 64
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
  7. 91
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  8. 181
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs
  9. 34
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  10. 146
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  11. 313
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  12. 24
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  13. 13
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
  14. 15
      src/ImageSharp/Image{TPixel}.cs
  15. 2
      tests/Directory.Build.targets
  16. 54
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  17. 6
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToCieLabConvert.cs
  18. 6
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToHunterLabConvert.cs
  19. 5
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs
  20. 5
      tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs
  21. 9
      tests/ImageSharp.Benchmarks/Color/RgbWorkingSpaceAdapt.cs
  22. 1
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  23. 34
      tests/ImageSharp.Tests/Common/NumericsTests.cs
  24. 8
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
  25. 14
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  26. 97
      tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs
  27. 14
      tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
  28. 118
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  29. 69
      tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
  30. 10
      tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
  31. 45
      tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
  32. 8
      tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs
  33. 2
      tests/ImageSharp.Tests/TestImages.cs
  34. 4
      tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png
  35. 3
      tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg
  36. 3
      tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg

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

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

313
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -30,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.
@ -42,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>
@ -97,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>
@ -117,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.
@ -152,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>
@ -213,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.
@ -256,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
@ -287,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
@ -378,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)
{
@ -405,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;
}
@ -551,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))
@ -585,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)
@ -630,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();
@ -645,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)
{
@ -657,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)
{
@ -681,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++;
@ -698,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
@ -835,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;
@ -910,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);
}
}
@ -978,8 +939,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
i += 17 + codeLengthSum;
this.BuildHuffmanTable(
tableType == 0 ? this.dcHuffmanTables : this.acHuffmanTables,
this.scanDecoder.BuildHuffmanTable(
tableType,
tableIndex,
codeLengthsSpan,
huffmanValuesSpan);
@ -1002,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>
@ -1087,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;
}
}
}

24
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -506,11 +506,15 @@ namespace SixLabors.ImageSharp.Formats.Png
while (this.currentRow < this.header.Height)
{
Span<byte> scanlineSpan = this.scanline.GetSpan();
int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
this.currentRowBytesRead += bytesRead;
if (this.currentRowBytesRead < this.bytesPerScanline)
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;
@ -577,11 +581,15 @@ namespace SixLabors.ImageSharp.Formats.Png
while (this.currentRow < this.header.Height)
{
int bytesRead = compressedStream.Read(this.scanline.GetSpan(), 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;

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)

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}"/>.

2
tests/Directory.Build.targets

@ -20,7 +20,7 @@
<!-- Test Dependencies -->
<PackageReference Update="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="2.0.5" />
<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" />

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

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

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics;
using SixLabors.ImageSharp.Tests.Formats.Jpg;
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations;
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks;

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

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:

14
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;
@ -63,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; }
@ -79,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.
@ -132,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 =>
{
@ -164,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 =>
{

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)

2
tests/ImageSharp.Tests/TestImages.cs

@ -262,6 +262,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";
}
}

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
Loading…
Cancel
Save