Browse Source

Merge branch 'main' into dp/jpeg-colorspace-deduction

pull/2177/head
Dmitry Pentin 4 years ago
parent
commit
2273f20005
  1. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
  2. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs
  3. 11
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs
  4. 4
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs
  5. 6
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs
  6. 65
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs
  7. 73
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs
  8. 102
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs
  9. 102
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs
  10. 88
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs
  11. 12
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  12. 129
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
  13. 3
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  14. 71
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  15. 150
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  16. 14
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  17. 2
      src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs
  18. 23
      src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs
  19. 220
      src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs
  20. 38
      src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
  21. 41
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  22. 17
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs
  23. 4
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  24. 88
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs
  25. 103
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs
  26. 31
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
  27. 18
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  28. 204
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  29. 19
      tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs
  30. 80
      tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs
  31. 11
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  32. 21
      tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs

5
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs

@ -247,8 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.scanBuffer = new JpegBitReader(this.stream);
bool fullScan = this.frame.Progressive || this.frame.MultiScan;
this.frame.AllocateComponents(fullScan);
this.frame.AllocateComponents();
if (this.frame.Progressive)
{
@ -326,11 +325,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
if (this.scanComponentCount != 1)
{
this.spectralConverter.PrepareForDecoding();
this.ParseBaselineDataInterleaved();
this.spectralConverter.CommitConversion();
}
else if (this.frame.ComponentCount == 1)
{
this.spectralConverter.PrepareForDecoding();
this.ParseBaselineDataSingleComponent();
this.spectralConverter.CommitConversion();
}

5
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
@ -25,7 +26,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
}
public override bool IsAvailable => Avx.IsSupported;
public sealed override bool IsAvailable => Avx.IsSupported;
public sealed override int ElementsPerBatch => Vector256<float>.Count;
}
}
}

11
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs

@ -35,6 +35,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// </summary>
public abstract bool IsAvailable { get; }
/// <summary>
/// Gets a value indicating how many pixels are processed in a single batch.
/// </summary>
/// <remarks>
/// This generally should be equal to register size,
/// e.g. 1 for scalar implementation, 8 for AVX implementation and so on.
/// </remarks>
public abstract int ElementsPerBatch { get; }
/// <summary>
/// Gets the <see cref="JpegColorSpace"/> of this converter.
/// </summary>
@ -219,7 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// </summary>
/// <param name="processors">List of component color processors.</param>
/// <param name="row">Row to convert</param>
public ComponentValues(IReadOnlyList<JpegComponentPostProcessor> processors, int row)
public ComponentValues(IReadOnlyList<ComponentProcessor> processors, int row)
{
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs

@ -16,7 +16,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
}
public override bool IsAvailable => true;
public sealed override bool IsAvailable => true;
public sealed override int ElementsPerBatch => 1;
}
}
}

6
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// Even though real life data is guaranteed to be of size
/// divisible by 8 newer SIMD instructions like AVX512 won't work with
/// such data out of the box. These converters have fallback code
/// for 'remainder' data.
/// for remainder data.
/// </remarks>
internal abstract class JpegColorConverterVector : JpegColorConverterBase
{
@ -28,7 +28,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
public sealed override bool IsAvailable => Vector.IsHardwareAccelerated && Vector<float>.Count % 4 == 0;
public override void ConvertToRgbInplace(in ComponentValues values)
public sealed override int ElementsPerBatch => Vector<float>.Count;
public sealed override void ConvertToRgbInplace(in ComponentValues values)
{
DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware.");

65
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Base class for processing component spectral data and converting it to raw color data.
/// </summary>
internal abstract class ComponentProcessor : IDisposable
{
public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize)
{
this.Frame = frame;
this.Component = component;
this.BlockAreaSize = component.SubSamplingDivisors * blockSize;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.BlockAreaSize.Height);
}
protected JpegFrame Frame { get; }
protected IJpegComponent Component { get; }
protected Buffer2D<float> ColorBuffer { get; }
protected Size BlockAreaSize { get; }
/// <summary>
/// Converts spectral data to color data accessible via <see cref="GetColorBufferRowSpan(int)"/>.
/// </summary>
/// <param name="row">Spectral row index to convert.</param>
public abstract void CopyBlocksToColorBuffer(int row);
/// <summary>
/// Clears spectral buffers.
/// </summary>
/// <remarks>
/// Should only be called during baseline interleaved decoding.
/// </remarks>
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.Component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}
/// <summary>
/// Gets converted color buffer row.
/// </summary>
/// <param name="row">Row index.</param>
/// <returns>Color buffer row.</returns>
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);
public void Dispose() => this.ColorBuffer.Dispose();
}
}

73
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs

@ -0,0 +1,73 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Processes component spectral data and converts it to color data in 1-to-1 scale.
/// </summary>
internal sealed class DirectComponentProcessor : ComponentProcessor
{
private Block8x8F dequantizationTable;
public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8)
{
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex];
FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable);
}
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
int yBlockStart = spectralStep * blocksRowsPerStep;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;
Block8x8F workspaceBlock = default;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// Dequantize
workspaceBlock.MultiplyInPlace(ref this.dequantizationTable);
// Convert from spectral to color
FloatingPointDCT.TransformIDCT(ref workspaceBlock);
// To conform better to libjpeg we actually NEED TO loose precision here.
// This is because they store blocks as Int16 between all the operations.
// To be "more accurate", we need to emulate this by rounding!
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
// Write to color buffer acording to sampling factors
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
workspaceBlock.ScaledCopyTo(
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
}
}

102
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs

@ -0,0 +1,102 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Processes component spectral data and converts it to color data in 2-to-1 scale.
/// </summary>
internal sealed class DownScalingComponentProcessor2 : ComponentProcessor
{
private Block8x8F dequantizationTable;
public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, 4)
{
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex];
ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable);
}
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
float normalizationValue = MathF.Ceiling(maximumValue / 2);
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;
Block8x8F workspaceBlock = default;
int yBlockStart = spectralStep * blocksRowsPerStep;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// IDCT/Normalization/Range
ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue);
// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
ScaledCopyTo(
ref workspaceBlock,
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale)
{
// TODO: Optimize: implement all cases with scale-specific, loopless code!
CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale);
[MethodImpl(InliningOptions.ColdPath)]
static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale)
{
for (int y = 0; y < 4; y++)
{
int yy = y * verticalScale;
int y8 = y * 8;
for (int x = 0; x < 4; x++)
{
int xx = x * horizontalScale;
float value = block[y8 + x];
for (int i = 0; i < verticalScale; i++)
{
int baseIdx = ((yy + i) * areaStride) + xx;
for (int j = 0; j < horizontalScale; j++)
{
// area[xx + j, yy + i] = value;
Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value;
}
}
}
}
}
}
}
}

102
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs

@ -0,0 +1,102 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Processes component spectral data and converts it to color data in 4-to-1 scale.
/// </summary>
internal sealed class DownScalingComponentProcessor4 : ComponentProcessor
{
private Block8x8F dequantizationTable;
public DownScalingComponentProcessor4(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, 2)
{
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex];
ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable);
}
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
float normalizationValue = MathF.Ceiling(maximumValue / 2);
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;
Block8x8F workspaceBlock = default;
int yBlockStart = spectralStep * blocksRowsPerStep;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// IDCT/Normalization/Range
ScaledFloatingPointDCT.TransformIDCT_2x2(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue);
// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
ScaledCopyTo(
ref workspaceBlock,
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale)
{
// TODO: Optimize: implement all cases with scale-specific, loopless code!
CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale);
[MethodImpl(InliningOptions.ColdPath)]
static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale)
{
for (int y = 0; y < 2; y++)
{
int yy = y * verticalScale;
int y8 = y * 8;
for (int x = 0; x < 2; x++)
{
int xx = x * horizontalScale;
float value = block[y8 + x];
for (int i = 0; i < verticalScale; i++)
{
int baseIdx = ((yy + i) * areaStride) + xx;
for (int j = 0; j < horizontalScale; j++)
{
// area[xx + j, yy + i] = value;
Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value;
}
}
}
}
}
}
}
}

88
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs

@ -0,0 +1,88 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Processes component spectral data and converts it to color data in 8-to-1 scale.
/// </summary>
internal sealed class DownScalingComponentProcessor8 : ComponentProcessor
{
private readonly float dcDequantizatizer;
public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, 1)
=> this.dcDequantizatizer = 0.125f * rawJpeg.QuantizationTables[component.QuantizationTableIndex][0];
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
float normalizationValue = MathF.Ceiling(maximumValue / 2);
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;
int yBlockStart = spectralStep * blocksRowsPerStep;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
float dc = ScaledFloatingPointDCT.TransformIDCT_1x1(blockRow[xBlock][0], this.dcDequantizatizer, normalizationValue, maximumValue);
// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
ScaledCopyTo(
dc,
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void ScaledCopyTo(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale)
{
if (horizontalScale == 1 && verticalScale == 1)
{
destRef = value;
return;
}
if (horizontalScale == 2 && verticalScale == 2)
{
destRef = value;
Unsafe.Add(ref destRef, 1) = value;
Unsafe.Add(ref destRef, 0 + (nint)(uint)destStrideWidth) = value;
Unsafe.Add(ref destRef, 1 + (nint)(uint)destStrideWidth) = value;
return;
}
// TODO: Optimize: implement all cases with scale-specific, loopless code!
for (int y = 0; y < verticalScale; y++)
{
for (int x = 0; x < horizontalScale; x++)
{
Unsafe.Add(ref destRef, (nint)(uint)x) = value;
}
destRef = ref Unsafe.Add(ref destRef, (nint)(uint)destStrideWidth);
}
}
}
}

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

@ -109,10 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// The successive approximation low bit end.
public int SuccessiveLow { get; set; }
/// <summary>
/// Decodes the entropy coded data.
/// </summary>
/// <param name="scanComponentCount">Component count in the current scan.</param>
/// <inheritdoc/>
public void ParseEntropyCodedData(int scanComponentCount)
{
this.cancellationToken.ThrowIfCancellationRequested();
@ -121,8 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.scanBuffer = new JpegBitReader(this.stream);
bool fullScan = this.frame.Progressive || this.frame.MultiScan;
this.frame.AllocateComponents(fullScan);
this.frame.AllocateComponents();
if (!this.frame.Progressive)
{
@ -152,11 +148,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
if (this.scanComponentCount != 1)
{
this.spectralConverter.PrepareForDecoding();
this.ParseBaselineDataInterleaved();
this.spectralConverter.CommitConversion();
}
else if (this.frame.ComponentCount == 1)
{
this.spectralConverter.PrepareForDecoding();
this.ParseBaselineDataSingleComponent();
this.spectralConverter.CommitConversion();
}
@ -269,7 +267,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseBaselineDataSingleComponent()
{
var component = this.frame.Components[0] as JpegComponent;
JpegComponent component = this.frame.Components[0];
int mcuLines = this.frame.McusPerColumn;
int w = component.WidthInBlocks;
int h = component.SamplingFactors.Height;

129
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs

@ -1,129 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Encapsulates spectral data to rgba32 processing for one component.
/// </summary>
internal class JpegComponentPostProcessor : IDisposable
{
/// <summary>
/// The size of the area in <see cref="ColorBuffer"/> corresponding to one 8x8 Jpeg block
/// </summary>
private readonly Size blockAreaSize;
/// <summary>
/// Jpeg frame instance containing required decoding metadata.
/// </summary>
private readonly JpegFrame frame;
/// <summary>
/// Gets the maximal number of block rows being processed in one step.
/// </summary>
private readonly int blockRowsPerStep;
/// <summary>
/// Gets the <see cref="IJpegComponent"/> component containing decoding meta information.
/// </summary>
private readonly IJpegComponent component;
/// <summary>
/// Gets the <see cref="IRawJpegData"/> instance containing decoding meta information.
/// </summary>
private readonly IRawJpegData rawJpeg;
/// <summary>
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor"/> class.
/// </summary>
public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
{
this.frame = frame;
this.component = component;
this.rawJpeg = rawJpeg;
this.blockAreaSize = this.component.SubSamplingDivisors * 8;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.blockAreaSize.Height);
this.blockRowsPerStep = postProcessorBufferSize.Height / 8 / this.component.SubSamplingDivisors.Height;
}
/// <summary>
/// Gets the temporary working buffer of color values.
/// </summary>
public Buffer2D<float> ColorBuffer { get; }
/// <inheritdoc />
public void Dispose() => this.ColorBuffer.Dispose();
/// <summary>
/// Convert raw spectral DCT data to color data and copy it to the color buffer <see cref="ColorBuffer"/>.
/// </summary>
public void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.component.SpectralBlocks;
float maximumValue = this.frame.MaxColorChannelValue;
int destAreaStride = this.ColorBuffer.Width;
int yBlockStart = spectralStep * this.blockRowsPerStep;
Size subSamplingDivisors = this.component.SubSamplingDivisors;
Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex];
Block8x8F workspaceBlock = default;
for (int y = 0; y < this.blockRowsPerStep; y++)
{
int yBuffer = y * this.blockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// Dequantize
workspaceBlock.MultiplyInPlace(ref dequantTable);
// Convert from spectral to color
FastFloatingPointDCT.TransformIDCT(ref workspaceBlock);
// To conform better to libjpeg we actually NEED TO loose precision here.
// This is because they store blocks as Int16 between all the operations.
// To be "more accurate", we need to emulate this by rounding!
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
// Write to color buffer according to sampling factors
int xColorBufferStart = xBlock * this.blockAreaSize.Width;
workspaceBlock.ScaledCopyTo(
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);
}
}

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

@ -139,8 +139,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
public void AllocateComponents(bool fullScan)
public void AllocateComponents()
{
bool fullScan = this.Progressive || this.MultiScan;
for (int i = 0; i < this.ComponentCount; i++)
{
IJpegComponent component = this.Components[i];

71
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs

@ -10,6 +10,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
internal abstract class SpectralConverter
{
/// <summary>
/// Supported scaled spectral block sizes for scaled IDCT decoding.
/// </summary>
private static readonly int[] ScaledBlockSizes = new int[]
{
// 8 => 1, 1/8 of the original size
1,
// 8 => 2, 1/4 of the original size
2,
// 8 => 4, 1/2 of the original size
4,
};
/// <summary>
/// Gets a value indicating whether this converter has converted spectral
/// data of the current image or not.
@ -20,12 +35,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Injects jpeg image decoding metadata.
/// </summary>
/// <remarks>
/// This is guaranteed to be called only once at SOF marker by <see cref="HuffmanScanDecoder"/>.
/// This should be called exactly once during SOF (Start Of Frame) marker.
/// </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>
/// <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>
/// Initializes this spectral decoder instance for decoding.
/// This should be called exactly once after all markers which can alter
/// spectral decoding parameters.
/// </summary>
public abstract void PrepareForDecoding();
/// <summary>
/// Converts single spectral jpeg stride to color stride in baseline
/// decoding mode.
@ -58,5 +80,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="jpegData">The raw JPEG data.</param>
/// <returns>The color converter.</returns>
protected virtual JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(jpegData.ColorSpace, frame.Precision);
/// <summary>
/// Calculates image size with optional scaling.
/// </summary>
/// <remarks>
/// Does not apply scalling if <paramref name="targetSize"/> is null.
/// </remarks>
/// <param name="size">Size of the image.</param>
/// <param name="targetSize">Target size of the image.</param>
/// <param name="blockPixelSize">Spectral block size, equals to 8 if scaling is not applied.</param>
/// <returns>Resulting image size, equals to <paramref name="size"/> if scaling is not applied.</returns>
public static Size CalculateResultingImageSize(Size size, Size? targetSize, out int blockPixelSize)
{
const int blockNativePixelSize = 8;
blockPixelSize = blockNativePixelSize;
if (targetSize != null)
{
Size tSize = targetSize.Value;
int fullBlocksWidth = (int)((uint)size.Width / blockNativePixelSize);
int fullBlocksHeight = (int)((uint)size.Height / blockNativePixelSize);
// & (blockNativePixelSize - 1) is Numerics.Modulo8(), basically
int blockWidthRemainder = size.Width & (blockNativePixelSize - 1);
int blockHeightRemainder = size.Height & (blockNativePixelSize - 1);
for (int i = 0; i < ScaledBlockSizes.Length; i++)
{
int blockSize = ScaledBlockSizes[i];
int scaledWidth = (fullBlocksWidth * blockSize) + (int)Numerics.DivideCeil((uint)(blockWidthRemainder * blockSize), blockNativePixelSize);
int scaledHeight = (fullBlocksHeight * blockSize) + (int)Numerics.DivideCeil((uint)(blockHeightRemainder * blockSize), blockNativePixelSize);
if (scaledWidth >= tSize.Width && scaledHeight >= tSize.Height)
{
blockPixelSize = blockSize;
return new Size(scaledWidth, scaledHeight);
}
}
}
return size;
}
}
}

150
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -31,10 +31,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
private readonly Configuration configuration;
private JpegFrame frame;
private IRawJpegData jpegData;
/// <summary>
/// Jpeg component converters from decompressed spectral to color data.
/// </summary>
private JpegComponentPostProcessor[] componentProcessors;
private ComponentProcessor[] componentProcessors;
/// <summary>
/// Color converter from jpeg color space to target pixel color space.
@ -66,13 +70,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
private int pixelRowCounter;
/// <summary>
/// Represent target size after decoding for scaling decoding mode.
/// </summary>
/// <remarks>
/// Null if no scaling is required.
/// </remarks>
private Size? targetSize;
/// <summary>
/// Initializes a new instance of the <see cref="SpectralConverter{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
public SpectralConverter(Configuration configuration) =>
/// <param name="targetSize">Optional target size for decoded image.</param>
public SpectralConverter(Configuration configuration, Size? targetSize = null)
{
this.configuration = configuration;
this.targetSize = targetSize;
}
/// <summary>
/// Gets converted pixel buffer.
/// </summary>
@ -86,6 +103,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
if (!this.Converted)
{
this.PrepareForDecoding();
int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep);
for (int step = 0; step < steps; step++)
@ -100,55 +119,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
return buffer;
}
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
// iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height);
const int blockPixelHeight = 8;
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight;
// pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(
frame.PixelWidth,
frame.PixelHeight,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(frame.PixelWidth + 3);
// component processors from spectral to Rgba32
const int blockPixelWidth = 8;
var postProcessorBufferSize = new Size(majorBlockWidth * blockPixelWidth, 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.rgbBuffer = allocator.Allocate<byte>(frame.PixelWidth * 3);
// color converter from Rgba32 to TPixel
this.colorConverter = this.GetColorConverter(frame, jpegData);
}
/// <inheritdoc/>
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.ConvertStride(spectralStep: 0);
foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
{
cpp.ClearSpectralBuffers();
}
}
/// <summary>
/// Converts single spectral jpeg stride to color stride.
/// </summary>
@ -199,12 +169,88 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.pixelRowCounter += this.pixelRowsPerStep;
}
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
this.frame = frame;
this.jpegData = jpegData;
}
/// <inheritdoc/>
public override void PrepareForDecoding()
{
DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once.");
MemoryAllocator allocator = this.configuration.MemoryAllocator;
// color converter from RGB to TPixel
JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData);
this.colorConverter = converter;
// resulting image size
Size pixelSize = CalculateResultingImageSize(this.frame.PixelSize, this.targetSize, out int blockPixelSize);
// iteration data
int majorBlockWidth = this.frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = this.frame.Components.Max((component) => component.SamplingFactors.Height);
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize;
// pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(
pixelSize.Width,
pixelSize.Height,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3);
// component processors from spectral to RGB
int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch;
int batchRemainder = bufferWidth & (batchSize - 1);
var postProcessorBufferSize = new Size(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep);
this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize);
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
this.rgbBuffer = allocator.Allocate<byte>(pixelSize.Width * 3);
}
/// <inheritdoc/>
public override void ConvertStrideBaseline()
{
// Convert next pixel stride using single spectral `stride'
// Note that zero passing eliminates extra virtual call
this.ConvertStride(spectralStep: 0);
foreach (ComponentProcessor cpp in this.componentProcessors)
{
cpp.ClearSpectralBuffers();
}
}
protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
var componentProcessors = new ComponentProcessor[frame.Components.Length];
for (int i = 0; i < componentProcessors.Length; i++)
{
componentProcessors[i] = blockPixelSize switch
{
4 => new DownScalingComponentProcessor2(allocator, frame, jpegData, processorBufferSize, frame.Components[i]),
2 => new DownScalingComponentProcessor4(allocator, frame, jpegData, processorBufferSize, frame.Components[i]),
1 => new DownScalingComponentProcessor8(allocator, frame, jpegData, processorBufferSize, frame.Components[i]),
_ => new DirectComponentProcessor(allocator, frame, jpegData, processorBufferSize, frame.Components[i]),
};
}
return componentProcessors;
}
/// <inheritdoc/>
public void Dispose()
{
if (this.componentProcessors != null)
{
foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
foreach (ComponentProcessor cpp in this.componentProcessors)
{
cpp.Dispose();
}

14
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -139,8 +139,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable);
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable);
this.huffmanTables = HuffmanLut.TheHuffmanLut;
@ -202,8 +202,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable);
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable);
this.huffmanTables = HuffmanLut.TheHuffmanLut;
@ -271,7 +271,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable);
this.huffmanTables = HuffmanLut.TheHuffmanLut;
@ -319,7 +319,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
public void EncodeRgb<TPixel>(Image<TPixel> pixels, ref Block8x8F quantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
FastFloatingPointDCT.AdjustToFDCT(ref quantTable);
FloatingPointDCT.AdjustToFDCT(ref quantTable);
this.huffmanTables = HuffmanLut.TheHuffmanLut;
@ -391,7 +391,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
block.AddInPlace(-128f);
// Discrete cosine transform
FastFloatingPointDCT.TransformFDCT(ref block);
FloatingPointDCT.TransformFDCT(ref block);
// Quantization
Block8x8F.Quantize(ref block, ref spectralBlock, ref quant);

2
src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs → src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs

@ -7,7 +7,7 @@ using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal static partial class FastFloatingPointDCT
internal static partial class FloatingPointDCT
{
/// <summary>
/// Apply floating point FDCT inplace using simd operations.

23
src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs → src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs

@ -12,9 +12,12 @@ using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
/// <summary>
/// Contains inaccurate, but fast forward and inverse DCT implementations.
/// Contains floating point forward and inverse DCT implementations
/// </summary>
internal static partial class FastFloatingPointDCT
/// <remarks>
/// Based on "Arai, Agui and Nakajima" algorithm.
/// </remarks>
internal static partial class FloatingPointDCT
{
#pragma warning disable SA1310, SA1311, IDE1006 // naming rules violation warnings
private static readonly Vector4 mm128_F_0_7071 = new(0.707106781f);
@ -70,8 +73,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
ref float multipliersRef = ref MemoryMarshal.GetReference<float>(AdjustmentCoefficients);
for (nint i = 0; i < Block8x8F.Size; i++)
{
tableRef = 0.125f * tableRef * Unsafe.Add(ref multipliersRef, i);
tableRef = ref Unsafe.Add(ref tableRef, 1);
ref float elemRef = ref Unsafe.Add(ref tableRef, i);
elemRef = 0.125f * elemRef * Unsafe.Add(ref multipliersRef, i);
}
// Spectral macroblocks are transposed before quantization
@ -89,8 +92,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
ref float multipliersRef = ref MemoryMarshal.GetReference<float>(AdjustmentCoefficients);
for (nint i = 0; i < Block8x8F.Size; i++)
{
tableRef = 0.125f / (tableRef * Unsafe.Add(ref multipliersRef, i));
tableRef = ref Unsafe.Add(ref tableRef, 1);
ref float elemRef = ref Unsafe.Add(ref tableRef, i);
elemRef = 0.125f / (elemRef * Unsafe.Add(ref multipliersRef, i));
}
// Spectral macroblocks are not transposed before quantization
@ -103,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// Apply 2D floating point IDCT inplace.
/// </summary>
/// <remarks>
/// Input block must be dequantized before this method with table
/// Input block must be dequantized with quantization table
/// adjusted by <see cref="AdjustToIDCT"/>.
/// </remarks>
/// <param name="block">Input block.</param>
@ -125,8 +128,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// Apply 2D floating point IDCT inplace.
/// </summary>
/// <remarks>
/// Input block must be quantized after this method with table adjusted
/// by <see cref="AdjustToFDCT"/>.
/// Input block must be quantized after this method with quantization
/// table adjusted by <see cref="AdjustToFDCT"/>.
/// </remarks>
/// <param name="block">Input block.</param>
public static void TransformFDCT(ref Block8x8F block)
@ -221,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// Apply floating point FDCT inplace using <see cref="Vector4"/> API.
/// </summary>
/// <param name="block">Input block.</param>
public static void FDCT_Vector4(ref Block8x8F block)
private static void FDCT_Vector4(ref Block8x8F block)
{
// First pass - process columns
FDCT8x4_Vector4(ref block.V0L);

220
src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs

@ -0,0 +1,220 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
#pragma warning disable IDE0078
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
/// <summary>
/// Contains floating point forward DCT implementations with built-in scaling.
/// </summary>
/// <remarks>
/// Based on "Loeffler, Ligtenberg, and Moschytz" algorithm.
/// </remarks>
internal static class ScaledFloatingPointDCT
{
#pragma warning disable SA1310
private const float FP32_0_541196100 = 0.541196100f;
private const float FP32_0_765366865 = 0.765366865f;
private const float FP32_1_847759065 = 1.847759065f;
private const float FP32_0_211164243 = 0.211164243f;
private const float FP32_1_451774981 = 1.451774981f;
private const float FP32_2_172734803 = 2.172734803f;
private const float FP32_1_061594337 = 1.061594337f;
private const float FP32_0_509795579 = 0.509795579f;
private const float FP32_0_601344887 = 0.601344887f;
private const float FP32_0_899976223 = 0.899976223f;
private const float FP32_2_562915447 = 2.562915447f;
private const float FP32_0_720959822 = 0.720959822f;
private const float FP32_0_850430095 = 0.850430095f;
private const float FP32_1_272758580 = 1.272758580f;
private const float FP32_3_624509785 = 3.624509785f;
#pragma warning restore SA1310
/// <summary>
/// Adjusts given quantization table for usage with IDCT algorithms
/// from <see cref="ScaledFloatingPointDCT"/>.
/// </summary>
/// <param name="quantTable">Quantization table to adjust.</param>
public static void AdjustToIDCT(ref Block8x8F quantTable)
{
ref float tableRef = ref Unsafe.As<Block8x8F, float>(ref quantTable);
for (nint i = 0; i < Block8x8F.Size; i++)
{
ref float elemRef = ref Unsafe.Add(ref tableRef, i);
elemRef = 0.125f * elemRef;
}
// Spectral macroblocks are transposed before quantization
// so we must transpose quantization table
quantTable.TransposeInplace();
}
/// <summary>
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
/// 8x8 -> 4x4 result.
/// </summary>
/// <remarks>
/// Resulting matrix is stored in the top left 4x4 part of the
/// <paramref name="block"/>.
/// </remarks>
/// <param name="block">Input block.</param>
/// <param name="dequantTable">Dequantization table adjusted by <see cref="AdjustToIDCT(ref Block8x8F)"/>.</param>
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
/// <param name="maxValue">Maximum value of the output range.</param>
public static void TransformIDCT_4x4(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue)
{
for (int ctr = 0; ctr < 8; ctr++)
{
// Don't process row 4, second pass doesn't use it
if (ctr == 4)
{
continue;
}
// Even part
float tmp0 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0] * 2;
float z2 = block[(ctr * 8) + 2] * dequantTable[(ctr * 8) + 2];
float z3 = block[(ctr * 8) + 6] * dequantTable[(ctr * 8) + 6];
float tmp2 = (z2 * FP32_1_847759065) + (z3 * -FP32_0_765366865);
float tmp10 = tmp0 + tmp2;
float tmp12 = tmp0 - tmp2;
// Odd part
float z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7];
z2 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5];
z3 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3];
float z4 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1];
tmp0 = (z1 * -FP32_0_211164243) +
(z2 * FP32_1_451774981) +
(z3 * -FP32_2_172734803) +
(z4 * FP32_1_061594337);
tmp2 = (z1 * -FP32_0_509795579) +
(z2 * -FP32_0_601344887) +
(z3 * FP32_0_899976223) +
(z4 * FP32_2_562915447);
// temporal result is saved to +4 shifted indices
// because result is saved into the top left 2x2 region of the
// input block
block[(ctr * 8) + 0 + 4] = (tmp10 + tmp2) / 2;
block[(ctr * 8) + 3 + 4] = (tmp10 - tmp2) / 2;
block[(ctr * 8) + 1 + 4] = (tmp12 + tmp0) / 2;
block[(ctr * 8) + 2 + 4] = (tmp12 - tmp0) / 2;
}
for (int ctr = 0; ctr < 4; ctr++)
{
// Even part
float tmp0 = block[ctr + (8 * 0) + 4] * 2;
float tmp2 = (block[ctr + (8 * 2) + 4] * FP32_1_847759065) + (block[ctr + (8 * 6) + 4] * -FP32_0_765366865);
float tmp10 = tmp0 + tmp2;
float tmp12 = tmp0 - tmp2;
// Odd part
float z1 = block[ctr + (8 * 7) + 4];
float z2 = block[ctr + (8 * 5) + 4];
float z3 = block[ctr + (8 * 3) + 4];
float z4 = block[ctr + (8 * 1) + 4];
tmp0 = (z1 * -FP32_0_211164243) +
(z2 * FP32_1_451774981) +
(z3 * -FP32_2_172734803) +
(z4 * FP32_1_061594337);
tmp2 = (z1 * -FP32_0_509795579) +
(z2 * -FP32_0_601344887) +
(z3 * FP32_0_899976223) +
(z4 * FP32_2_562915447);
// Save results to the top left 4x4 subregion
block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp2) / 2) + normalizationValue, 0, maxValue));
block[(ctr * 8) + 3] = MathF.Round(Numerics.Clamp(((tmp10 - tmp2) / 2) + normalizationValue, 0, maxValue));
block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp12 + tmp0) / 2) + normalizationValue, 0, maxValue));
block[(ctr * 8) + 2] = MathF.Round(Numerics.Clamp(((tmp12 - tmp0) / 2) + normalizationValue, 0, maxValue));
}
}
/// <summary>
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
/// 8x8 -> 2x2 result.
/// </summary>
/// <remarks>
/// Resulting matrix is stored in the top left 2x2 part of the
/// <paramref name="block"/>.
/// </remarks>
/// <param name="block">Input block.</param>
/// <param name="dequantTable">Dequantization table adjusted by <see cref="AdjustToIDCT(ref Block8x8F)"/>.</param>
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
/// <param name="maxValue">Maximum value of the output range.</param>
public static void TransformIDCT_2x2(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue)
{
for (int ctr = 0; ctr < 8; ctr++)
{
// Don't process rows 2/4/6, second pass doesn't use it
if (ctr == 2 || ctr == 4 || ctr == 6)
{
continue;
}
// Even part
float tmp0;
float z1 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0];
float tmp10 = z1 * 4;
// Odd part
z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7];
tmp0 = z1 * -FP32_0_720959822;
z1 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5];
tmp0 += z1 * FP32_0_850430095;
z1 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3];
tmp0 += z1 * -FP32_1_272758580;
z1 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1];
tmp0 += z1 * FP32_3_624509785;
// temporal result is saved to +2 shifted indices
// because result is saved into the top left 2x2 region of the
// input block
block[(ctr * 8) + 2] = (tmp10 + tmp0) / 4;
block[(ctr * 8) + 3] = (tmp10 - tmp0) / 4;
}
for (int ctr = 0; ctr < 2; ctr++)
{
// Even part
float tmp10 = block[ctr + (8 * 0) + 2] * 4;
// Odd part
float tmp0 = (block[ctr + (8 * 7) + 2] * -FP32_0_720959822) +
(block[ctr + (8 * 5) + 2] * FP32_0_850430095) +
(block[ctr + (8 * 3) + 2] * -FP32_1_272758580) +
(block[ctr + (8 * 1) + 2] * FP32_3_624509785);
// Save results to the top left 2x2 subregion
block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp0) / 4) + normalizationValue, 0, maxValue));
block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp10 - tmp0) / 4) + normalizationValue, 0, maxValue));
}
}
/// <summary>
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
/// 8x8 -> 1x1 result.
/// </summary>
/// <param name="dc">Direct current term value from input block.</param>
/// <param name="dequantizer">Dequantization value.</param>
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
/// <param name="maxValue">Maximum value of the output range.</param>
public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue)
=> MathF.Round(Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue));
}
}
#pragma warning restore IDE0078

38
src/ImageSharp/Formats/Jpeg/JpegDecoder.cs

@ -3,6 +3,8 @@
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -29,6 +31,42 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgb24>(configuration, stream, cancellationToken);
/// <summary>
/// Placeholder summary.
/// </summary>
/// <param name="configuration">Placeholder2</param>
/// <param name="stream">Placeholder3</param>
/// <param name="targetSize">Placeholder4</param>
/// <param name="cancellationToken">Placeholder5</param>
/// <returns>Placeholder6</returns>
internal Image DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken)
=> this.DecodeInto<Rgb24>(configuration, stream, targetSize, cancellationToken);
/// <summary>
/// Decodes and downscales the image from the specified stream if possible.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">Configuration.</param>
/// <param name="stream">Stream.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="cancellationToken">Cancellation token.</param>
internal Image<TPixel> DecodeInto<TPixel>(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
using var decoder = new JpegDecoderCore(configuration, this);
using var bufferedReadStream = new BufferedReadStream(configuration, stream);
try
{
return decoder.DecodeInto<TPixel>(bufferedReadStream, targetSize, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{
throw new InvalidImageContentException(((IImageDecoderInternals)decoder).Dimensions, ex);
}
}
/// <inheritdoc/>
public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken)
{

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

@ -201,34 +201,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var spectralConverter = new SpectralConverter<TPixel>(this.Configuration);
=> this.Decode<TPixel>(stream, targetSize: null, cancellationToken);
this.ParseStream(stream, spectralConverter, cancellationToken);
/// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseStream(stream, spectralConverter: null, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
return new Image<TPixel>(
this.Configuration,
spectralConverter.GetPixelBuffer(cancellationToken),
this.Metadata);
Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
}
/// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
/// <summary>
/// Decodes and downscales the image from the specified stream if possible.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">Stream.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="cancellationToken">Cancellation token.</param>
internal Image<TPixel> DecodeInto<TPixel>(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> this.Decode<TPixel>(stream, targetSize, cancellationToken);
private Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Size? targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ParseStream(stream, spectralConverter: null, cancellationToken);
using var spectralConverter = new SpectralConverter<TPixel>(this.Configuration, targetSize);
this.ParseStream(stream, spectralConverter, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
return new Image<TPixel>(
this.Configuration,
spectralConverter.GetPixelBuffer(cancellationToken),
this.Metadata);
}
/// <summary>
@ -1138,9 +1152,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break;
}
}
// Adjusting table for IDCT step during decompression
FastFloatingPointDCT.AdjustToIDCT(ref table);
}
}

17
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs

@ -79,4 +79,21 @@ Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
| 'Baseline 4:2:0 Interleaved' | 8.458 ms | 0.0289 ms | 0.0256 ms |
| 'Baseline 4:0:0 (grayscale)' | 1.550 ms | 0.0050 ms | 0.0044 ms |
| 'Progressive 4:2:0 Non-Interleaved' | 13.220 ms | 0.0449 ms | 0.0398 ms |
FRESH BENCHMARKS FOR NEW SPECTRAL CONVERSION SETUP
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044
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.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT
DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT
| Method | Mean | Error | StdDev |
|------------------------------------ |----------:|----------:|----------:|
| 'Baseline 4:4:4 Interleaved' | 10.734 ms | 0.0287 ms | 0.0254 ms |
| 'Baseline 4:2:0 Interleaved' | 8.517 ms | 0.0401 ms | 0.0356 ms |
| 'Baseline 4:0:0 (grayscale)' | 1.442 ms | 0.0051 ms | 0.0045 ms |
| 'Progressive 4:2:0 Non-Interleaved' | 12.740 ms | 0.0832 ms | 0.0730 ms |
*/

4
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs

@ -57,6 +57,10 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
}
public override void PrepareForDecoding()
{
}
}
}
}

88
tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs

@ -1,88 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
[Config(typeof(Config.ShortMultiFramework))]
public class LoadResizeSave_Aggregate : MultiImageBenchmarkBase
{
protected override IEnumerable<string> InputImageSubfoldersOrFiles =>
new[]
{
TestImages.Jpeg.BenchmarkSuite.Jpeg400_SmallMonochrome,
TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr,
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr,
TestImages.Jpeg.BenchmarkSuite.MissingFF00ProgressiveBedroom159_MidSize420YCbCr,
TestImages.Jpeg.BenchmarkSuite.ExifGetString750Transform_Huge420YCbCr,
};
[Params(InputImageCategory.AllImages)]
public override InputImageCategory InputCategory { get; set; }
private readonly Configuration configuration = new Configuration(new JpegConfigurationModule());
private byte[] destBytes;
public override void Setup()
{
base.Setup();
this.configuration.MaxDegreeOfParallelism = 1;
const int MaxOutputSizeInBytes = 2 * 1024 * 1024; // ~2 MB
this.destBytes = new byte[MaxOutputSizeInBytes];
}
[Benchmark(Baseline = true)]
public void SystemDrawing()
=> this.ForEachStream(
sourceStream =>
{
using (var destStream = new MemoryStream(this.destBytes))
using (var source = System.Drawing.Image.FromStream(sourceStream))
using (var destination = new Bitmap(source.Width / 4, source.Height / 4))
{
using (var g = Graphics.FromImage(destination))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;
g.DrawImage(source, 0, 0, 400, 400);
}
destination.Save(destStream, ImageFormat.Jpeg);
}
return null;
});
[Benchmark]
public void ImageSharp()
=> this.ForEachStream(
sourceStream =>
{
using (var source = Image.Load<Rgba32>(
this.configuration,
sourceStream,
new JpegDecoder { IgnoreMetadata = true }))
{
using var destStream = new MemoryStream(this.destBytes);
source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4));
source.SaveAsJpeg(destStream);
}
return null;
});
}
}

103
tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs

@ -1,103 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests;
using SDImage = System.Drawing.Image;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
[Config(typeof(Config.ShortMultiFramework))]
public class LoadResizeSave_ImageSpecific
{
private readonly Configuration configuration = new Configuration(new JpegConfigurationModule());
private byte[] sourceBytes;
private byte[] destBytes;
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage);
[Params(
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr,
TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr,
TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr)]
public string TestImage { get; set; }
[Params(false, true)]
public bool ParallelExec { get; set; }
[GlobalSetup]
public void Setup()
{
this.configuration.MaxDegreeOfParallelism =
this.ParallelExec ? Environment.ProcessorCount : 1;
this.sourceBytes = File.ReadAllBytes(this.TestImageFullPath);
this.destBytes = new byte[this.sourceBytes.Length * 2];
}
[Benchmark(Baseline = true)]
public void SystemDrawing()
{
using var sourceStream = new MemoryStream(this.sourceBytes);
using var destStream = new MemoryStream(this.destBytes);
using var source = SDImage.FromStream(sourceStream);
using var destination = new Bitmap(source.Width / 4, source.Height / 4);
using (var g = Graphics.FromImage(destination))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;
g.DrawImage(source, 0, 0, 400, 400);
}
destination.Save(destStream, ImageFormat.Jpeg);
}
[Benchmark]
public void ImageSharp()
{
using (var source = Image.Load(this.configuration, this.sourceBytes, new JpegDecoder { IgnoreMetadata = true }))
using (var destStream = new MemoryStream(this.destBytes))
{
source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4));
source.SaveAsJpeg(destStream);
}
}
// RESULTS (2018 October 31):
//
// BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
// Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
// Frequency=2742191 Hz, Resolution=364.6719 ns, Timer=TSC
// .NET Core SDK=2.1.403
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
// Job-ZPEZGV : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
// Job-SGOCJT : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
//
// Method | Runtime | TestImage | ParallelExec | Mean | Error | StdDev | Scaled | ScaledSD | Gen 0 | Allocated |
// -------------- |-------- |----------------------------- |------------- |----------:|----------:|----------:|-------:|---------:|---------:|----------:|
// SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | False | 64.88 ms | 3.735 ms | 0.2110 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB |
// ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | False | 129.53 ms | 23.423 ms | 1.3234 ms | 2.00 | 0.02 | - | 50.09 KB |
// | | | | | | | | | | |
// SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | False | 65.87 ms | 10.488 ms | 0.5926 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB |
// ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | False | 92.00 ms | 7.241 ms | 0.4091 ms | 1.40 | 0.01 | - | 46.36 KB |
// | | | | | | | | | | |
// SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | True | 64.23 ms | 5.998 ms | 0.3389 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB |
// ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | True | 82.63 ms | 29.320 ms | 1.6566 ms | 1.29 | 0.02 | - | 57.59 KB |
// | | | | | | | | | | |
// SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | True | 64.20 ms | 6.560 ms | 0.3707 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB |
// ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | True | 68.08 ms | 18.376 ms | 1.0383 ms | 1.06 | 0.01 | - | 50.49 KB |
}
}

31
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs

@ -3,7 +3,6 @@
using System;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
{
@ -37,10 +36,10 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
public int[] ParallelismValues { get; } =
{
Environment.ProcessorCount,
// Environment.ProcessorCount,
// Environment.ProcessorCount / 2,
// Environment.ProcessorCount / 4,
// 1
1
};
[Benchmark]
@ -49,10 +48,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(ParallelismValues))]
public void ImageSharp(int maxDegreeOfParallelism)
{
this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
}
public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
[Benchmark]
[ArgumentsSource(nameof(ParallelismValues))]
@ -75,3 +71,24 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
public void NetVips(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.NetVipsResize, maxDegreeOfParallelism);
}
}
/*
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.300
[Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
ShortRun : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | maxDegreeOfParallelism | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------------- |----------------------- |-----------:|------------:|----------:|------:|--------:|------:|------:|------:|----------:|
| SystemDrawing | 1 | 3,624.2 ms | 721.39 ms | 39.54 ms | 3.30 | 0.04 | - | - | - | 12 KB |
| ImageSharp | 1 | 1,098.4 ms | 45.64 ms | 2.50 ms | 1.00 | 0.00 | - | - | - | 717 KB |
| Magick | 1 | 4,089.8 ms | 905.06 ms | 49.61 ms | 3.72 | 0.04 | - | - | - | 43 KB |
| MagicScaler | 1 | 888.0 ms | 168.33 ms | 9.23 ms | 0.81 | 0.01 | - | - | - | 105 KB |
| SkiaBitmap | 1 | 2,934.4 ms | 2,023.43 ms | 110.91 ms | 2.67 | 0.10 | - | - | - | 43 KB |
| SkiaBitmapDecodeToTargetSize | 1 | 892.3 ms | 115.54 ms | 6.33 ms | 0.81 | 0.01 | - | - | - | 48 KB |
| NetVips | 1 | 806.8 ms | 86.23 ms | 4.73 ms | 0.73 | 0.01 | - | - | - | 42 KB |
*/

18
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -204,23 +204,30 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
public void ImageSharpResize(string input)
{
using FileStream output = File.Open(this.OutputPath(input), FileMode.Create);
using FileStream inputStream = File.Open(input, FileMode.Open);
using FileStream outputStream = File.Open(this.OutputPath(input), FileMode.Create);
// Resize it to fit a 150x150 square
using var image = ImageSharpImage.Load(input);
var targetSize = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize);
var decoder = new JpegDecoder();
using ImageSharpImage image = decoder.DecodeInto(Configuration.Default, inputStream, targetSize, default);
this.LogImageProcessed(image.Width, image.Height);
image.Mutate(i => i.Resize(new ResizeOptions
{
Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize),
Mode = ResizeMode.Max
Size = targetSize,
Mode = ResizeMode.Max,
Sampler = KnownResamplers.Box
}));
// Reduce the size of the file
image.Metadata.ExifProfile = null;
image.Metadata.XmpProfile = null;
image.Metadata.IccProfile = null;
image.Metadata.IptcProfile = null;
// Save the results
image.Save(output, this.imageSharpJpegEncoder);
image.Save(outputStream, this.imageSharpJpegEncoder);
}
public async Task ImageSharpResizeAsync(string input)
@ -231,6 +238,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
using ImageSharpImage image = await ImageSharpImage.LoadAsync(input);
this.LogImageProcessed(image.Width, image.Height);
// Resize checks whether image size and target sizes are equal
image.Mutate(i => i.Resize(new ResizeOptions
{
Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize),

204
tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs

@ -3,6 +3,7 @@
using System;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
using SixLabors.ImageSharp.Tests.TestUtilities;
using Xunit;
@ -14,8 +15,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")]
public static class DCTTests
{
private const int MaxAllowedValue = short.MaxValue;
private const int MinAllowedValue = short.MinValue;
// size of input values is 10 bit max
private const float MaxInputValue = 1023;
private const float MinInputValue = -1024;
// output value range is 12 bit max
private const float MaxOutputValue = 4096;
private const float NormalizationValue = MaxOutputValue / 2;
internal static Block8x8F CreateBlockFromScalar(float value)
{
@ -41,7 +47,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(3)]
public void LLM_TransformIDCT_CompareToNonOptimized(int seed)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed);
float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed);
var srcBlock = Block8x8F.Load(sourceArray);
@ -56,14 +62,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Block8x8F dequantMatrix = CreateBlockFromScalar(1);
// This step is needed to apply adjusting multipliers to the input block
FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
FloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
// IDCT implementation tranforms blocks after transposition
srcBlock.TransposeInplace();
srcBlock.MultiplyInPlace(ref dequantMatrix);
// IDCT calculation
FastFloatingPointDCT.TransformIDCT(ref srcBlock);
FloatingPointDCT.TransformIDCT(ref srcBlock);
this.CompareBlocks(expected, srcBlock, 1f);
}
@ -74,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(3)]
public void LLM_TransformIDCT_CompareToAccurate(int seed)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed);
float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed);
var srcBlock = Block8x8F.Load(sourceArray);
@ -89,21 +95,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Block8x8F dequantMatrix = CreateBlockFromScalar(1);
// This step is needed to apply adjusting multipliers to the input block
FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
FloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
// IDCT implementation tranforms blocks after transposition
srcBlock.TransposeInplace();
srcBlock.MultiplyInPlace(ref dequantMatrix);
// IDCT calculation
FastFloatingPointDCT.TransformIDCT(ref srcBlock);
FloatingPointDCT.TransformIDCT(ref srcBlock);
this.CompareBlocks(expected, srcBlock, 1f);
}
// Inverse transform
// This test covers entire IDCT conversion chain
// This test checks all hardware implementations
[Theory]
[InlineData(1)]
[InlineData(2)]
@ -113,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
int seed = FeatureTestRunner.Deserialize<int>(serialized);
Span<float> src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed);
Span<float> src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
@ -132,13 +135,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// Dequantization using unit matrix - no values are upscaled
// as quant matrix is all 1's
// This step is needed to apply adjusting multipliers to the input block
FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
FloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
srcBlock.MultiplyInPlace(ref dequantMatrix);
// testee
// IDCT implementation tranforms blocks after transposition
srcBlock.TransposeInplace();
FastFloatingPointDCT.TransformIDCT(ref srcBlock);
FloatingPointDCT.TransformIDCT(ref srcBlock);
float[] actualDest = srcBlock.ToArray();
@ -156,9 +159,170 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
// Forward transform
// This test covers entire FDCT conversion chain
// This test checks all hardware implementations
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TranformIDCT_4x4(int seed)
{
Span<float> src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 4, 4);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
float[] expectedDest = new float[64];
float[] temp = new float[64];
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp);
// testee
// Part of the IDCT calculations is fused into the quantization step
// We must multiply input block with adjusted no-quantization matrix
// before applying IDCT
Block8x8F dequantMatrix = CreateBlockFromScalar(1);
// Dequantization using unit matrix - no values are upscaled
// as quant matrix is all 1's
// This step is needed to apply adjusting multipliers to the input block
ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
// testee
// IDCT implementation tranforms blocks after transposition
srcBlock.TransposeInplace();
ScaledFloatingPointDCT.TransformIDCT_4x4(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue);
Span<float> expectedSpan = expectedDest.AsSpan();
Span<float> actualSpan = srcBlock.ToArray().AsSpan();
// resulting matrix is 4x4
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
AssertScaledElementEquality(expectedSpan.Slice((y * 16) + (x * 2)), actualSpan.Slice((y * 8) + x));
}
}
static void AssertScaledElementEquality(Span<float> expected, Span<float> actual)
{
float average2x2 = 0f;
for (int y = 0; y < 2; y++)
{
int y8 = y * 8;
for (int x = 0; x < 2; x++)
{
float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue);
average2x2 += clamped;
}
}
average2x2 = MathF.Round(average2x2 / 4f);
Assert.Equal((int)average2x2, (int)actual[0]);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TranformIDCT_2x2(int seed)
{
Span<float> src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 2, 2);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
float[] expectedDest = new float[64];
float[] temp = new float[64];
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp);
// testee
// Part of the IDCT calculations is fused into the quantization step
// We must multiply input block with adjusted no-quantization matrix
// before applying IDCT
Block8x8F dequantMatrix = CreateBlockFromScalar(1);
// Dequantization using unit matrix - no values are upscaled
// as quant matrix is all 1's
// This step is needed to apply adjusting multipliers to the input block
ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
// testee
// IDCT implementation tranforms blocks after transposition
srcBlock.TransposeInplace();
ScaledFloatingPointDCT.TransformIDCT_2x2(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue);
Span<float> expectedSpan = expectedDest.AsSpan();
Span<float> actualSpan = srcBlock.ToArray().AsSpan();
// resulting matrix is 2x2
for (int y = 0; y < 2; y++)
{
for (int x = 0; x < 2; x++)
{
AssertScaledElementEquality(expectedSpan.Slice((y * 32) + (x * 4)), actualSpan.Slice((y * 8) + x));
}
}
static void AssertScaledElementEquality(Span<float> expected, Span<float> actual)
{
float average4x4 = 0f;
for (int y = 0; y < 4; y++)
{
int y8 = y * 8;
for (int x = 0; x < 4; x++)
{
float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue);
average4x4 += clamped;
}
}
average4x4 = MathF.Round(average4x4 / 16f);
Assert.Equal((int)average4x4, (int)actual[0]);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TranformIDCT_1x1(int seed)
{
Span<float> src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 1, 1);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
float[] expectedDest = new float[64];
float[] temp = new float[64];
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp);
// testee
// Part of the IDCT calculations is fused into the quantization step
// We must multiply input block with adjusted no-quantization matrix
// before applying IDCT
Block8x8F dequantMatrix = CreateBlockFromScalar(1);
// Dequantization using unit matrix - no values are upscaled
// as quant matrix is all 1's
// This step is needed to apply adjusting multipliers to the input block
ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix);
// testee
// IDCT implementation tranforms blocks after transposition
// But DC lays on main diagonal which is not changed by transposition
float actual = ScaledFloatingPointDCT.TransformIDCT_1x1(
srcBlock[0],
dequantMatrix[0],
NormalizationValue,
MaxOutputValue);
float expected = MathF.Round(Numerics.Clamp(expectedDest[0] + NormalizationValue, 0, MaxOutputValue));
Assert.Equal((int)actual, (int)expected);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
@ -168,7 +332,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
int seed = FeatureTestRunner.Deserialize<int>(serialized);
Span<float> src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed);
Span<float> src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed);
var block = default(Block8x8F);
block.LoadFrom(src);
@ -181,14 +345,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// testee
// Second transpose call is done by Quantize step
// Do this manually here just to be complient to the reference implementation
FastFloatingPointDCT.TransformFDCT(ref block);
FloatingPointDCT.TransformFDCT(ref block);
block.TransposeInplace();
// Part of the IDCT calculations is fused into the quantization step
// We must multiply input block with adjusted no-quantization matrix
// after applying FDCT
Block8x8F quantMatrix = CreateBlockFromScalar(1);
FastFloatingPointDCT.AdjustToFDCT(ref quantMatrix);
FloatingPointDCT.AdjustToFDCT(ref quantMatrix);
block.MultiplyInPlace(ref quantMatrix);
float[] actualDest = block.ToArray();

19
tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs

@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(2, 200)]
public void LLM_IDCT_IsEquivalentTo_AccurateImplementation(int seed, int range)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(-range, range, seed);
float[] sourceArray = Create8x8RandomFloatData(-range, range, seed);
var source = Block8x8F.Load(sourceArray);
@ -64,23 +64,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
this.CompareBlocks(expected, actual, 0.1f);
}
[Theory]
[InlineData(42, 1000)]
[InlineData(1, 1000)]
[InlineData(2, 1000)]
public void LLM_IDCT_CompareToIntegerRoundedAccurateImplementation(int seed, int range)
{
Block8x8F fSource = CreateRoundedRandomFloatBlock(-range, range, seed);
Block8x8 iSource = fSource.RoundAsInt16Block();
Block8x8 iExpected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref iSource);
Block8x8F fExpected = iExpected.AsFloatBlock();
Block8x8F fActual = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref fSource);
this.CompareBlocks(fExpected, fActual, 2);
}
[Theory]
[InlineData(42)]
[InlineData(1)]

80
tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs

@ -0,0 +1,80 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
[Trait("Format", "Jpg")]
public class SpectralConverterTests
{
// Test for null target size, i.e. when no scaling is needed
[Theory]
[InlineData(1, 1)]
[InlineData(800, 400)]
[InlineData(2354, 4847)]
public void CalculateResultingImageSize_Null_TargetSize(int width, int height)
{
Size inputSize = new(width, height);
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, null, out int blockPixelSize);
Assert.Equal(expected: 8, blockPixelSize);
Assert.Equal(inputSize, outputSize);
}
// Test for 'perfect' dimensions, i.e. dimensions divisible by 8, with exact scaled size match
[Theory]
[InlineData(800, 400, 800, 400, 8)]
[InlineData(800, 400, 400, 200, 4)]
[InlineData(800, 400, 200, 100, 2)]
[InlineData(800, 400, 100, 50, 1)]
public void CalculateResultingImageSize_Perfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize)
{
Size inputSize = new(inW, inH);
Size targetSize = new(tW, tH);
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize);
Assert.Equal(expectedBlockSize, blockPixelSize);
Assert.Equal(outputSize, targetSize);
}
// Test for 'imperfect' dimensions, i.e. dimensions NOT divisible by 8, with exact scaled size match
[Theory]
[InlineData(7, 14, 7, 14, 8)]
[InlineData(7, 14, 4, 7, 4)]
[InlineData(7, 14, 2, 4, 2)]
[InlineData(7, 14, 1, 2, 1)]
public void CalculateResultingImageSize_Imperfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize)
{
Size inputSize = new(inW, inH);
Size targetSize = new(tW, tH);
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize);
Assert.Equal(expectedBlockSize, blockPixelSize);
Assert.Equal(outputSize, targetSize);
}
// Test for inexact target and output sizes match
[Theory]
[InlineData(7, 14, 4, 6, 4, 7, 4)]
[InlineData(7, 14, 1, 1, 1, 2, 1)]
[InlineData(800, 400, 999, 600, 800, 400, 8)]
[InlineData(800, 400, 390, 150, 400, 200, 4)]
[InlineData(804, 1198, 500, 800, 804, 1198, 8)]
public void CalculateResultingImageSize_Inexact_Target_Size(int inW, int inH, int tW, int tH, int exW, int exH, int expectedBlockSize)
{
Size inputSize = new(inW, inH);
Size targetSize = new(tW, tH);
Size expectedSize = new(exW, exH);
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize);
Assert.Equal(expectedBlockSize, blockPixelSize);
Assert.Equal(expectedSize, outputSize);
}
}
}

11
tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs

@ -141,6 +141,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
private JpegFrame frame;
private IRawJpegData jpegData;
private LibJpegTools.SpectralData spectralData;
private int baselineScanRowCounter;
@ -153,6 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// Progressive and multi-scan images must be loaded manually
if (this.frame.Progressive || this.frame.MultiScan)
{
this.PrepareForDecoding();
LibJpegTools.ComponentData[] components = this.spectralData.Components;
for (int i = 0; i < components.Length; i++)
{
@ -190,11 +193,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
this.frame = frame;
this.jpegData = jpegData;
}
var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount];
public override void PrepareForDecoding()
{
var spectralComponents = new LibJpegTools.ComponentData[this.frame.ComponentCount];
for (int i = 0; i < spectralComponents.Length; i++)
{
var component = frame.Components[i] as JpegComponent;
JpegComponent component = this.frame.Components[i];
spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index);
}

21
tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs

@ -89,33 +89,28 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
return result;
}
internal static float[] Create8x8RoundedRandomFloatData(int minValue, int maxValue, int seed = 42)
=> Create8x8RandomIntData(minValue, maxValue, seed).ConvertAllToFloat();
public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42)
public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8)
{
var rnd = new Random(seed);
var result = new float[64];
for (int i = 0; i < 8; i++)
float[] result = new float[64];
for (int y = 0; y < yBorder; y++)
{
for (int j = 0; j < 8; j++)
int y8 = y * 8;
for (int x = 0; x < xBorder; x++)
{
double val = rnd.NextDouble();
val *= maxValue - minValue;
val += minValue;
result[(i * 8) + j] = (float)val;
result[y8 + x] = (float)val;
}
}
return result;
}
internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42) =>
Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed));
internal static Block8x8F CreateRoundedRandomFloatBlock(int minValue, int maxValue, int seed = 42) =>
Block8x8F.Load(Create8x8RoundedRandomFloatData(minValue, maxValue, seed));
internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8) =>
Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed, xBorder, yBorder));
internal void Print8x8Data<T>(T[] data) => this.Print8x8Data(new Span<T>(data));

Loading…
Cancel
Save