Browse Source

Merge branch 'main' into bp/Issue2171

pull/2173/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
a21c90e28b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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. 152
      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. 156
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  22. 7
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  23. 31
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
  24. 50
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
  25. 55
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  26. 1
      src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
  27. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  28. 4
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  29. 8
      src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs
  30. 49
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs
  31. 46
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs
  32. 18
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  33. 12
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
  34. 3
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs
  35. 3
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs
  36. 7
      src/ImageSharp/Formats/Tiff/README.md
  37. 26
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  38. 2
      src/ImageSharp/ImageSharp.csproj
  39. 2
      src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs
  40. 17
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs
  41. 4
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  42. 88
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs
  43. 103
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs
  44. 31
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
  45. 18
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  46. 204
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  47. 71
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs
  48. 19
      tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs
  49. 80
      tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs
  50. 11
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  51. 21
      tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs
  52. 25
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  53. 7
      tests/ImageSharp.Tests/TestImages.cs
  54. 3
      tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png
  55. 3
      tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png
  56. 3
      tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png
  57. 3
      tests/Images/Input/Tiff/CieLab.tiff
  58. 3
      tests/Images/Input/Tiff/CieLabPlanar.tiff
  59. 3
      tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff
  60. 3
      tests/Images/Input/Tiff/webp_compressed.tiff

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

152
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++)
@ -95,60 +114,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
var buffer = this.pixelBuffer;
Buffer2D<TPixel> buffer = this.pixelBuffer;
this.pixelBuffer = null;
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)
{

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

@ -86,14 +86,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private byte[] xmpData;
/// <summary>
/// Contains information about the JFIF marker.
/// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace.
/// </summary>
private JFifMarker jFif;
private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr.
/// Contains information about the JFIF marker.
/// </summary>
private bool hasJFif;
private JFifMarker jFif;
/// <summary>
/// Contains information about the Adobe marker.
@ -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>
@ -503,11 +517,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <summary>
/// Returns the correct colorspace based on the image component count and the jpeg frame component id's.
/// Returns encoded colorspace based on the adobe APP14 marker.
/// </summary>
/// <param name="componentCount">The number of components.</param>
/// <param name="componentCount">Number of components.</param>
/// <param name="adobeMarker">Parsed adobe APP14 marker.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns>
private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker)
{
if (componentCount == 1)
{
@ -516,80 +531,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (componentCount == 3)
{
// We prioritize adobe marker over jfif marker, if somebody really encoded this image with redundant adobe marker,
// then it's most likely an adobe jfif image.
if (!this.adobe.Equals(default))
if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
{
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr)
{
return JpegColorSpace.YCbCr;
}
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
{
return JpegColorSpace.RGB;
}
// Fallback to the id color deduction: If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr.
if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1)
{
return JpegColorSpace.YCbCr;
}
JpegThrowHelper.ThrowNotSupportedColorSpace();
return JpegColorSpace.RGB;
}
if (this.hasJFif)
{
// JFIF implies YCbCr.
return JpegColorSpace.YCbCr;
}
return JpegColorSpace.YCbCr;
}
// Fallback to the id color deduction.
// If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr.
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82)
if (componentCount == 4)
{
if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformYcck)
{
return JpegColorSpace.RGB;
return JpegColorSpace.Ycck;
}
// If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr.
if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1)
{
return JpegColorSpace.YCbCr;
}
return JpegColorSpace.Cmyk;
}
// 3-channel non-subsampled images are assumed to be RGB.
if (this.Components[2].VerticalSamplingFactor == 1 && this.Components[1].VerticalSamplingFactor == 1 && this.Components[0].VerticalSamplingFactor == 1 &&
this.Components[2].HorizontalSamplingFactor == 1 && this.Components[1].HorizontalSamplingFactor == 1 && this.Components[0].HorizontalSamplingFactor == 1)
{
return JpegColorSpace.RGB;
}
JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount);
return default;
}
// Some images are poorly encoded and contain incorrect colorspace transform metadata.
// We ignore that and always fall back to the default colorspace.
/// <summary>
/// Returns encoded colorspace based on the component count.
/// </summary>
/// <param name="componentCount">Number of components.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns>
internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount)
{
if (componentCount == 1)
{
return JpegColorSpace.Grayscale;
}
if (componentCount == 3)
{
return JpegColorSpace.YCbCr;
}
if (componentCount == 4)
{
// jfif images doesn't not support 4 component images, so we only check adobe.
if (!this.adobe.Equals(default))
{
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck)
{
return JpegColorSpace.Ycck;
}
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
{
return JpegColorSpace.Cmyk;
}
JpegThrowHelper.ThrowNotSupportedColorSpace();
}
// Fallback to cmyk as neither of cmyk nor ycck have 'special' component ids.
return JpegColorSpace.Cmyk;
}
@ -757,8 +739,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining)
{
this.hasJFif = true;
// We can only decode JFif identifiers.
// Some images contain multiple JFIF markers (Issue 1932) so we check to see
// if it's already been read.
@ -1061,7 +1041,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.temp, 0, MarkerLength);
remaining -= MarkerLength;
AdobeMarker.TryParse(this.temp, out this.adobe);
if (AdobeMarker.TryParse(this.temp, out this.adobe))
{
this.hasAdobeMarker = true;
}
if (remaining > 0)
{
@ -1169,9 +1152,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break;
}
}
// Adjusting table for IDCT step during decompression
FastFloatingPointDCT.AdjustToIDCT(ref table);
}
}
@ -1257,7 +1237,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int maxH = 0;
int maxV = 0;
int index = 0;
for (int i = 0; i < componentCount; i++)
for (int i = 0; i < this.Frame.Components.Length; i++)
{
// 1 byte: component identifier
byte componentId = this.temp[index];
@ -1308,7 +1288,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index += componentBytes;
}
this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
this.ColorSpace = this.hasAdobeMarker
? DeduceJpegColorSpace(componentCount, ref this.adobe)
: DeduceJpegColorSpace(componentCount);
this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType();
if (!metadataOnly)

7
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs

@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(this.configuration);
using SpectralConverter<L8> spectralConverterGray =
new GrayJpegSpectralConverter<L8>(this.configuration);
var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);
@ -73,8 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using SpectralConverter<Rgb24> spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
new RgbJpegSpectralConverter<Rgb24>(this.configuration) : new SpectralConverter<Rgb24>(this.configuration);
using SpectralConverter<Rgb24> spectralConverter =
new TiffJpegSpectralConverter<Rgb24>(this.configuration, this.photometricInterpretation);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);

31
src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs

@ -1,31 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class RgbJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
public RgbJpegSpectralConverter(Configuration configuration)
: base(configuration)
{
}
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision);
}
}

50
src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class TiffJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TiffPhotometricInterpretation photometricInterpretation;
/// <summary>
/// Initializes a new instance of the <see cref="TiffJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="photometricInterpretation">Tiff photometric interpretation.</param>
public TiffJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation)
: base(configuration)
=> this.photometricInterpretation = photometricInterpretation;
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData)
{
JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation);
return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision);
}
/// <remarks>
/// This converter must be used only for RGB and YCbCr color spaces for performance reasons.
/// For grayscale images <see cref="GrayJpegSpectralConverter{TPixel}"/> must be used.
/// </remarks>
private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation)
=> interpretation switch
{
TiffPhotometricInterpretation.Rgb => JpegColorSpace.RGB,
TiffPhotometricInterpretation.YCbCr => JpegColorSpace.RGB,
_ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"),
};
}
}

55
src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed as a webp stream.
/// </summary>
internal class WebpTiffCompression : TiffBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="WebpTiffCompression"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="predictor">The predictor.</param>
public WebpTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
: base(memoryAllocator, width, bitsPerPixel, predictor)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer)
{
using var image = Image.Load<Rgb24>(stream, new WebpDecoder());
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
}
private static void CopyImageBytesToBuffer(Span<byte> buffer, Buffer2D<Rgb24> pixelBuffer)
{
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++)
{
Span<Rgb24> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
rgbBytes.CopyTo(buffer.Slice(offset));
offset += rgbBytes.Length;
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

1
src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs

@ -41,6 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
UndoGray32Bit(pixelBytes, width, isBigEndian);
break;
case TiffColorType.Rgb888:
case TiffColorType.CieLab:
UndoRgb24Bit(pixelBytes, width);
break;
case TiffColorType.Rgba8888:

5
src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs

@ -47,5 +47,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// The image data is compressed as a JPEG stream.
/// </summary>
Jpeg = 7,
/// <summary>
/// The image data is compressed as a WEBP stream.
/// </summary>
Webp = 8,
}
}

4
src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs

@ -60,6 +60,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation);
case TiffDecoderCompressionType.Webp:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new WebpTiffCompression(allocator, width, bitsPerPixel);
default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
}

8
src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs

@ -103,5 +103,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// if this is chosen.
/// </summary>
OldDeflate = 32946,
/// <summary>
/// Pixel data is compressed with webp encoder.
///
/// Note: The TIFF encoder does not support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
Webp = 50001,
}
}

49
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
/// </summary>
internal class CieLabPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorSpaceConverter ColorSpaceConverter = new();
private const float Inv255 = 1.0f / 255.0f;
/// <inheritdoc/>
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
Span<byte> l = data[0].GetSpan();
Span<byte> a = data[1].GetSpan();
Span<byte> b = data[2].GetSpan();
var color = default(TPixel);
int offset = 0;
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
for (int x = 0; x < pixelRow.Length; x++)
{
var lab = new CieLab((l[offset] & 0xFF) * 100f * Inv255, (sbyte)a[offset], (sbyte)b[offset]);
var rgb = ColorSpaceConverter.ToRgb(lab);
color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f));
pixelRow[x] = color;
offset++;
}
}
}
}
}

46
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
/// </summary>
internal class CieLabTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorSpaceConverter ColorSpaceConverter = new();
private const float Inv255 = 1.0f / 255.0f;
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
for (int x = 0; x < pixelRow.Length; x++)
{
float l = (data[offset] & 0xFF) * 100f * Inv255;
var lab = new CieLab(l, (sbyte)data[offset + 1], (sbyte)data[offset + 2]);
var rgb = ColorSpaceConverter.ToRgb(lab);
color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f));
pixelRow[x] = color;
offset += 3;
}
}
}
}
}

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

@ -385,8 +385,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
case TiffColorType.YCbCr:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 8
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
return new YCbCrTiffColor<TPixel>(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLab:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 8
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
return new CieLabTiffColor<TPixel>();
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
}
@ -415,6 +430,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
case TiffColorType.YCbCrPlanar:
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLabPlanar:
return new CieLabPlanarTiffColor<TPixel>();
case TiffColorType.Rgb161616Planar:
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb16PlanarTiffColor<TPixel>(byteOrder == ByteOrder.BigEndian);

12
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs

@ -276,6 +276,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
/// <summary>
/// The pixels are stored in YCbCr format as planar.
/// </summary>
YCbCrPlanar
YCbCrPlanar,
/// <summary>
/// The pixels are stored in CieLab format.
/// </summary>
CieLab,
/// <summary>
/// The pixels are stored in CieLab format as planar.
/// </summary>
CieLabPlanar,
}
}

3
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs

@ -9,6 +9,9 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'YCbCr' with the planar configuration.
/// </summary>
internal class YCbCrPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{

3
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs

@ -9,6 +9,9 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'YCbCr'.
/// </summary>
internal class YCbCrTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{

7
src/ImageSharp/Formats/Tiff/README.md

@ -37,11 +37,12 @@
|PackBits | Y | Y | |
|CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | Y | Y | |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case |
|Old Jpeg | | | We should not even try to support this |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. |
|Old Jpeg | | | We should not even try to support this. |
|Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |
|Webp | | Y | |
### Photometric Interpretation Formats
@ -55,7 +56,7 @@
|TransparencyMask | | | |
|Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | Y | |
|CieLab (TIFF Extension) | | | |
|CieLab (TIFF Extension) | | Y | |
|IccLab (TechNote 1) | | | |
### Baseline TIFF Tags

26
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -381,7 +381,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value;
if (options.BitsPerSample.Channels != 3)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for YCbCr images.");
}
ushort bitsPerChannel = options.BitsPerSample.Channel0;
@ -395,6 +395,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break;
}
case TiffPhotometricInterpretation.CieLab:
{
if (options.BitsPerSample.Channels != 3)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CieLab images.");
}
ushort bitsPerChannel = options.BitsPerSample.Channel0;
if (bitsPerChannel != 8)
{
TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for CieLab images.");
}
options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.CieLab : TiffColorType.CieLabPlanar;
break;
}
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}");
@ -470,6 +488,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break;
}
case TiffCompression.Webp:
{
options.CompressionType = TiffDecoderCompressionType.Webp;
break;
}
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported");

2
src/ImageSharp/ImageSharp.csproj

@ -24,11 +24,13 @@
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</Otherwise>
</Choose>

2
src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs

@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
public static string GetDescription(ExifTag tag, object value)
{
var tagValue = (ExifTagValue)(ushort)tag;
FieldInfo field = tagValue.GetType().GetTypeInfo().GetDeclaredField(tagValue.ToString());
FieldInfo field = typeof(ExifTagValue).GetField(tagValue.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (field is null)
{

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

71
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats.Jpeg;
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;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
using Xunit.Abstractions;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
[Trait("Format", "Jpg")]
public partial class JpegDecoderTests
{
[Theory]
[InlineData(1, 0, JpegColorSpace.Grayscale)]
[InlineData(3, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.RGB)]
[InlineData(3, JpegConstants.Adobe.ColorTransformYCbCr, JpegColorSpace.YCbCr)]
[InlineData(4, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.Cmyk)]
[InlineData(4, JpegConstants.Adobe.ColorTransformYcck, JpegColorSpace.Ycck)]
internal void DeduceJpegColorSpaceAdobeMarker_ShouldReturnValidColorSpace(byte componentCount, byte adobeFlag, JpegColorSpace expectedColorSpace)
{
byte[] adobeMarkerPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, adobeFlag };
ProfileResolver.AdobeMarker.CopyTo(adobeMarkerPayload);
_ = AdobeMarker.TryParse(adobeMarkerPayload, out AdobeMarker adobeMarker);
JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker);
Assert.Equal(expectedColorSpace, actualColorSpace);
}
[Theory]
[InlineData(2)]
[InlineData(5)]
public void DeduceJpegColorSpaceAdobeMarker_ShouldThrowOnUnsupportedComponentCount(byte componentCount)
{
AdobeMarker adobeMarker = default;
Assert.Throws<NotSupportedException>(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker));
}
[Theory]
[InlineData(1, JpegColorSpace.Grayscale)]
[InlineData(3, JpegColorSpace.YCbCr)]
[InlineData(4, JpegColorSpace.Cmyk)]
internal void DeduceJpegColorSpace_ShouldReturnValidColorSpace(byte componentCount, JpegColorSpace expectedColorSpace)
{
JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount);
Assert.Equal(expectedColorSpace, actualColorSpace);
}
[Theory]
[InlineData(2)]
[InlineData(5)]
public void DeduceJpegColorSpace_ShouldThrowOnUnsupportedComponentCount(byte componentCount)
=> Assert.Throws<NotSupportedException>(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount));
}
}

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

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

@ -318,6 +318,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
[Theory]
[WithFile(CieLab, PixelTypes.Rgba32)]
[WithFile(CieLabPlanar, PixelTypes.Rgba32)]
[WithFile(CieLabLzwPredictor, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_CieLab<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong
// converting the pixel data from Magick.NET to our format with CieLab?
using Image<TPixel> image = provider.GetImage();
image.DebugSave(provider);
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
[Theory]
[WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)]
@ -653,6 +667,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);
[Theory]
[WithFile(WebpCompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_WebpCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows)
{
TestTiffDecoder(provider, useExactComparer: false);
}
}
// https://github.com/SixLabors/ImageSharp/issues/1891
[Theory]
[WithFile(Issues1891, PixelTypes.Rgba32)]

7
tests/ImageSharp.Tests/TestImages.cs

@ -763,8 +763,8 @@ namespace SixLabors.ImageSharp.Tests
public const string Fax4Compressed = "Tiff/basi3p02_fax4.tiff";
public const string Fax4Compressed2 = "Tiff/CCITTGroup4.tiff";
public const string Fax4CompressedLowerOrderBitsFirst = "Tiff/basi3p02_fax4_lowerOrderBitsFirst.tiff";
public const string WebpCompressed = "Tiff/webp_compressed.tiff";
public const string Fax4CompressedMinIsBlack = "Tiff/CCITTGroup4_minisblack.tiff";
public const string CcittFax3AllTermCodes = "Tiff/ccitt_fax3_all_terminating_codes.tiff";
public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff";
public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff";
@ -914,6 +914,11 @@ namespace SixLabors.ImageSharp.Tests
public const string Rgba32BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff";
public const string Rgba32BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff";
// Cie Lab color space.
public const string CieLab = "Tiff/CieLab.tiff";
public const string CieLabPlanar = "Tiff/CieLabPlanar.tiff";
public const string CieLabLzwPredictor = "Tiff/CieLab_lzwcompressed_predictor.tiff";
public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string Issues1891 = "Tiff/Issues/Issue1891.tiff";
public const string Issues2123 = "Tiff/Issues/Issue2123.tiff";

3
tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png

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

3
tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png

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

3
tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png

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

3
tests/Images/Input/Tiff/CieLab.tiff

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

3
tests/Images/Input/Tiff/CieLabPlanar.tiff

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

3
tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff

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

3
tests/Images/Input/Tiff/webp_compressed.tiff

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