Browse Source

Add support for decoding jpeg's with arithmetic coding

pull/2073/head
Brian Popow 4 years ago
parent
commit
2c4b4cffcc
  1. 4
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 12
      src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs
  3. 30
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs
  4. 43
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs
  5. 1250
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
  6. 29
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs
  7. 57
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  8. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
  9. 48
      src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs
  10. 49
      src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs
  11. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
  12. 9
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
  13. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
  14. 16
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  15. 6
      src/ImageSharp/Formats/Jpeg/JpegConstants.cs
  16. 172
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  17. 4
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  18. 4
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  19. 7
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  20. 2
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  21. 34
      tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
  22. 6
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  23. 10
      tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
  24. 44
      tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
  25. 2
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  26. 2
      tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
  27. 32
      tests/ImageSharp.Tests/TestFormat.cs

4
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height);
public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
@ -382,7 +382,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
if (rowHasUndefinedPixels)
{
// Slow path with undefined pixels.
var yMulWidth = y * width;
int yMulWidth = y * width;
int rowStartIdx = yMulWidth * 3;
for (int x = 0; x < width; x++)
{

12
src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs

@ -0,0 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal enum ComponentType
{
Huffman = 0,
Arithmetic = 1
}
}

30
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal class ArithmeticDecodingComponent : JpegComponent
{
public ArithmeticDecodingComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id, int horizontalFactor, int verticalFactor, byte quantizationTableIndex, int index)
: base(memoryAllocator, frame, id, horizontalFactor, verticalFactor, quantizationTableIndex, index)
{
}
/// <summary>
/// Gets or sets the dc context.
/// </summary>
public int DcContext { get; set; }
/// <summary>
/// Gets or sets the dc statistics.
/// </summary>
public ArithmeticStatistics DcStatistics { get; set; }
/// <summary>
/// Gets or sets the ac statistics.
/// </summary>
public ArithmeticStatistics AcStatistics { get; set; }
}
}

43
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal class ArithmeticDecodingTable
{
public ArithmeticDecodingTable(byte tableClass, byte identifier)
{
this.TableClass = tableClass;
this.Identifier = identifier;
}
public byte TableClass { get; }
public byte Identifier { get; }
public byte ConditioningTableValue { get; private set; }
public int DcL { get; private set; }
public int DcU { get; private set; }
public int AcKx { get; private set; }
public void Configure(byte conditioningTableValue)
{
this.ConditioningTableValue = conditioningTableValue;
if (this.TableClass == 0)
{
this.DcL = conditioningTableValue & 0x0F;
this.DcU = conditioningTableValue >> 4;
this.AcKx = 0;
}
else
{
this.DcL = 0;
this.DcU = 0;
this.AcKx = conditioningTableValue;
}
}
}
}

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

File diff suppressed because it is too large

29
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal class ArithmeticStatistics
{
private readonly byte[] statistics;
public ArithmeticStatistics(bool dc, int identifier)
{
this.IsDcStatistics = dc;
this.Identifier = identifier;
this.statistics = dc ? new byte[64] : new byte[256];
}
public bool IsDcStatistics { get; private set; }
public int Identifier { get; private set; }
public ref byte GetReference() => ref this.statistics[0];
public ref byte GetReference(int offset) => ref this.statistics[offset];
public void Reset() => this.statistics.AsSpan().Clear();
}
}

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

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Originally ported from <see href="https://github.com/t0rakka/mango"/>
/// with additional fixes for both performance and common encoding errors.
/// </summary>
internal class HuffmanScanDecoder
internal class HuffmanScanDecoder : IJpegScanDecoder
{
private readonly BufferedReadStream stream;
@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Shortcut for <see cref="frame"/>.Components.
/// </summary>
private JpegComponent[] components;
private IJpegComponent[] components;
/// <summary>
/// Number of component in the current scan.
@ -54,11 +54,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private readonly HuffmanTable[] dcHuffmanTables;
/// <summary>
/// The AC Huffman tables
/// The AC Huffman tables.
/// </summary>
private readonly HuffmanTable[] acHuffmanTables;
private HuffmanScanBuffer scanBuffer;
private JpegBitReader scanBuffer;
private readonly SpectralConverter spectralConverter;
@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.scanComponentCount = scanComponentCount;
this.scanBuffer = new HuffmanScanBuffer(this.stream);
this.scanBuffer = new JpegBitReader(this.stream);
bool fullScan = this.frame.Progressive || this.frame.MultiScan;
this.frame.AllocateComponents(fullScan);
@ -139,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
/// <inheritdoc/>
public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
this.frame = frame;
@ -170,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int mcu = 0;
int mcusPerColumn = this.frame.McusPerColumn;
int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
for (int j = 0; j < mcusPerColumn; j++)
{
@ -184,10 +185,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
for (int k = 0; k < this.scanComponentCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
var component = this.components[order] as JpegComponent;
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
int h = component.HorizontalSamplingFactor;
int v = component.VerticalSamplingFactor;
@ -233,14 +234,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseBaselineDataNonInterleaved()
{
JpegComponent component = this.components[this.frame.ComponentOrder[0]];
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent;
ref JpegBitReader buffer = ref this.scanBuffer;
int w = component.WidthInBlocks;
int h = component.HeightInBlocks;
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int j = 0; j < h; j++)
{
@ -268,14 +269,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseBaselineDataSingleComponent()
{
JpegComponent component = this.frame.Components[0];
var component = this.frame.Components[0] as JpegComponent;
int mcuLines = this.frame.McusPerColumn;
int w = component.WidthInBlocks;
int h = component.SamplingFactors.Height;
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
for (int i = 0; i < mcuLines; i++)
{
@ -382,7 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int mcu = 0;
int mcusPerColumn = this.frame.McusPerColumn;
int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
for (int j = 0; j < mcusPerColumn; j++)
{
@ -394,8 +395,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
for (int k = 0; k < this.scanComponentCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
var component = this.components[order] as JpegComponent;
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
int h = component.HorizontalSamplingFactor;
int v = component.VerticalSamplingFactor;
@ -435,15 +436,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseProgressiveDataNonInterleaved()
{
JpegComponent component = this.components[this.frame.ComponentOrder[0]];
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent;
ref JpegBitReader buffer = ref this.scanBuffer;
int w = component.WidthInBlocks;
int h = component.HeightInBlocks;
if (this.SpectralStart == 0)
{
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
for (int j = 0; j < h; j++)
{
@ -470,7 +471,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
else
{
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int j = 0; j < h; j++)
{
@ -503,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanTable acTable)
{
ref short blockDataRef = ref Unsafe.As<Block8x8, short>(ref block);
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
// DC
int t = buffer.DecodeHuffman(ref dcTable);
@ -545,7 +546,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 block, ref HuffmanTable dcTable)
{
ref short blockDataRef = ref Unsafe.As<Block8x8, short>(ref block);
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
if (this.SuccessiveHigh == 0)
{
@ -581,7 +582,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
return;
}
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
int start = this.SpectralStart;
int end = this.SpectralEnd;
int low = this.SuccessiveLow;
@ -626,7 +627,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void DecodeBlockProgressiveACRefined(ref short blockDataRef, ref HuffmanTable acTable)
{
// Refinement scan for these AC coefficients
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref JpegBitReader buffer = ref this.scanBuffer;
int start = this.SpectralStart;
int end = this.SpectralEnd;

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs

@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to
/// ensure <see cref="HuffmanScanBuffer.DecodeHuffman"/> terminates.
/// ensure <see cref="JpegBitReader.DecodeHuffman"/> terminates.
/// </summary>
public fixed ulong MaxCode[18];

48
src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs

@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
internal interface IJpegComponent
{
/// <summary>
/// Gets the component id.
/// </summary>
public byte Id { get; }
/// <summary>
/// Gets the component's position in the components array.
/// </summary>
@ -25,6 +30,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
Size SamplingFactors { get; }
/// <summary>
/// Gets the horizontal sampling factor.
/// </summary>
public int HorizontalSamplingFactor { get; }
/// <summary>
/// Gets the vertical sampling factor.
/// </summary>
public int VerticalSamplingFactor { get; }
/// <summary>
/// Gets the divisors needed to apply when calculating colors.
/// <see>
@ -44,5 +59,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// We need to apply IDCT and dequantization to transform them into color-space blocks.
/// </summary>
Buffer2D<Block8x8> SpectralBlocks { get; }
/// <summary>
/// Gets or sets DC coefficient predictor.
/// </summary>
public int DcPredictor { get; set; }
/// <summary>
/// Gets or sets the index for the DC table.
/// </summary>
public int DcTableId { get; set; }
/// <summary>
/// Gets or sets the index for the AC table.
/// </summary>
public int AcTableId { get; set; }
/// <summary>
/// Initializes component for future buffers initialization.
/// </summary>
/// <param name="maxSubFactorH">Maximal horizontal subsampling factor among all the components.</param>
/// <param name="maxSubFactorV">Maximal vertical subsampling factor among all the components.</param>
public void Init(int maxSubFactorH, int maxSubFactorV);
/// <summary>
/// Allocates the spectral blocks.
/// </summary>
/// <param name="fullScan">if set to true, use the full height of a block, otherwise use the vertical sampling factor.</param>
public void AllocateSpectral(bool fullScan);
/// <summary>
/// Releases resources.
/// </summary>
public void Dispose();
}
}

49
src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Interface for a JPEG scan decoder.
/// </summary>
internal interface IJpegScanDecoder
{
/// <summary>
/// Sets the reset interval.
/// </summary>
int ResetInterval { set; }
/// <summary>
/// Gets or sets the spectral selection start.
/// </summary>
int SpectralStart { get; set; }
/// <summary>
/// Gets or sets the spectral selection end.
/// </summary>
int SpectralEnd { get; set; }
/// <summary>
/// Gets or sets the successive approximation high bit end.
/// </summary>
int SuccessiveHigh { get; set; }
/// <summary>
/// Gets or sets the successive approximation low bit end.
/// </summary>
int SuccessiveLow { get; set; }
/// <summary>
/// Decodes the entropy coded data.
/// </summary>
/// <param name="scanComponentCount">Component count in the current scan.</param>
void ParseEntropyCodedData(int scanComponentCount);
/// <summary>
/// Sets the JpegFrame and its components and injects the frame data into the spectral converter.
/// </summary>
/// <param name="frame">The frame.</param>
/// <param name="jpegData">The raw JPEG data.</param>
void InjectFrameData(JpegFrame frame, IRawJpegData jpegData);
}
}

7
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs → src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Used to buffer and track the bits read from the Huffman entropy encoded data.
/// </summary>
internal struct HuffmanScanBuffer
internal struct JpegBitReader
{
private readonly BufferedReadStream stream;
@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// Whether there is no more good data to pull from the stream for the current mcu.
private bool badData;
public HuffmanScanBuffer(BufferedReadStream stream)
public JpegBitReader(BufferedReadStream stream)
{
this.stream = stream;
this.data = 0ul;
@ -117,7 +117,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public int Receive(int nbits)
{
this.CheckBits();
return Extend(this.GetBits(nbits), nbits);
int bits = Extend(this.GetBits(nbits), nbits);
return bits;
}
[MethodImpl(InliningOptions.ShortMethod)]

9
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Represents a single frame component.
/// </summary>
internal sealed class JpegComponent : IDisposable, IJpegComponent
internal class JpegComponent : IDisposable, IJpegComponent
{
private readonly MemoryAllocator memoryAllocator;
@ -78,12 +78,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Gets or sets the index for the DC Huffman table.
/// </summary>
public int DCHuffmanTableId { get; set; }
public int DcTableId { get; set; }
/// <summary>
/// Gets or sets the index for the AC Huffman table.
/// </summary>
public int ACHuffmanTableId { get; set; }
public int AcTableId { get; set; }
public JpegFrame Frame { get; }
@ -119,11 +119,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
/// <inheritdoc/>
public void AllocateSpectral(bool fullScan)
{
if (this.SpectralBlocks != null)
{
// this method will be called each scan marker so we need to allocate only once
// This method will be called each scan marker so we need to allocate only once.
return;
}

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

@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// To be "more accurate", we need to emulate this by rounding!
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
// Write to color buffer acording to sampling factors
// Write to color buffer according to sampling factors
int xColorBufferStart = xBlock * this.blockAreaSize.Width;
workspaceBlock.ScaledCopyTo(
ref colorBufferRow[xColorBufferStart],

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

@ -6,14 +6,14 @@ using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Represent a single jpeg frame
/// Represent a single jpeg frame.
/// </summary>
internal sealed class JpegFrame : IDisposable
{
public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount)
{
this.Extended = sofMarker.Marker == JpegConstants.Markers.SOF1;
this.Progressive = sofMarker.Marker == JpegConstants.Markers.SOF2;
this.Extended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9;
this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10;
this.Precision = precision;
this.MaxColorChannelValue = MathF.Pow(2, precision) - 1;
@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Gets the pixel size of the image.
/// </summary>
public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight);
public Size PixelSize => new(this.PixelWidth, this.PixelHeight);
/// <summary>
/// Gets the number of components within a frame.
@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Gets or sets the frame component collection.
/// </summary>
public JpegComponent[] Components { get; set; }
public IJpegComponent[] Components { get; set; }
/// <summary>
/// Gets or sets the number of MCU's per line.
@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Gets the mcu size of the image.
/// </summary>
public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn);
public Size McuSize => new(this.McusPerLine, this.McusPerColumn);
/// <summary>
/// Gets the color depth, in number of bits per pixel.
@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
for (int i = 0; i < this.ComponentCount; i++)
{
JpegComponent component = this.Components[i];
IJpegComponent component = this.Components[i];
component.Init(maxSubFactorH, maxSubFactorV);
}
}
@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
for (int i = 0; i < this.ComponentCount; i++)
{
JpegComponent component = this.Components[i];
IJpegComponent component = this.Components[i];
component.AllocateSpectral(fullScan);
}
}

6
src/ImageSharp/Formats/Jpeg/JpegConstants.cs

@ -306,17 +306,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public const int RegisterSize = 64;
/// <summary>
/// The number of bits to fetch when filling the <see cref="HuffmanScanBuffer"/> buffer.
/// The number of bits to fetch when filling the <see cref="JpegBitReader"/> buffer.
/// </summary>
public const int FetchBits = 48;
/// <summary>
/// The number of times to read the input stream when filling the <see cref="HuffmanScanBuffer"/> buffer.
/// The number of times to read the input stream when filling the <see cref="JpegBitReader"/> buffer.
/// </summary>
public const int FetchLoop = FetchBits / 8;
/// <summary>
/// The minimum number of bits allowed before by the <see cref="HuffmanScanBuffer"/> before fetching.
/// The minimum number of bits allowed before by the <see cref="JpegBitReader"/> before fetching.
/// </summary>
public const int MinBits = RegisterSize - FetchBits;

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

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -97,7 +98,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Scan decoder.
/// </summary>
private HuffmanScanDecoder scanDecoder;
private IJpegScanDecoder scanDecoder;
/// <summary>
/// The arithmetic decoding tables.
/// </summary>
private List<ArithmeticDecodingTable> arithmeticDecodingTables;
/// <summary>
/// Initializes a new instance of the <see cref="JpegDecoderCore" /> class.
@ -137,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Gets the components.
/// </summary>
public JpegComponent[] Components => this.Frame.Components;
public IJpegComponent[] Components => this.Frame.Components;
/// <inheritdoc/>
IJpegComponent[] IRawJpegData.Components => this.Components;
@ -188,9 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
using var spectralConverter = new SpectralConverter<TPixel>(this.Configuration);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
this.ParseStream(stream, scanDecoder, cancellationToken);
this.ParseStream(stream, spectralConverter, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -206,7 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseStream(stream, scanDecoder: null, cancellationToken);
this.ParseStream(stream, spectralConverter: null, cancellationToken);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -222,12 +226,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips).
/// </summary>
/// <param name="tableBytes">The table bytes.</param>
/// <param name="huffmanScanDecoder">The scan decoder.</param>
public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder)
/// <param name="scanDecoder">The scan decoder.</param>
public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder)
{
this.Metadata = new ImageMetadata();
this.QuantizationTables = new Block8x8F[4];
this.scanDecoder = huffmanScanDecoder;
this.scanDecoder = scanDecoder;
using var ms = new MemoryStream(tableBytes);
using var stream = new BufferedReadStream(this.Configuration, ms);
@ -282,13 +286,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Parses the input stream for file markers.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="scanDecoder">Scan decoder used exclusively to decode SOS marker.</param>
/// <param name="spectralConverter">The spectral converter to use.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDecoder, CancellationToken cancellationToken)
internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralConverter, CancellationToken cancellationToken)
{
bool metadataOnly = scanDecoder == null;
this.scanDecoder = scanDecoder;
bool metadataOnly = spectralConverter == null;
this.Metadata = new ImageMetadata();
@ -322,7 +324,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOF0:
case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2:
this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly);
this.scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, ComponentType.Huffman, metadataOnly);
break;
case JpegConstants.Markers.SOF9:
case JpegConstants.Markers.SOF10:
case JpegConstants.Markers.SOF13:
case JpegConstants.Markers.SOF14:
this.scanDecoder = new ArithmeticScanDecoder(stream, spectralConverter, cancellationToken);
this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, ComponentType.Arithmetic, metadataOnly);
break;
case JpegConstants.Markers.SOF5:
@ -338,13 +349,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowNotSupportedException("Decoding lossless jpeg files is not supported.");
break;
case JpegConstants.Markers.SOF9:
case JpegConstants.Markers.SOF10:
case JpegConstants.Markers.SOF11:
case JpegConstants.Markers.SOF13:
case JpegConstants.Markers.SOF14:
case JpegConstants.Markers.SOF15:
JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with arithmetic coding is not supported.");
JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with lossless arithmetic coding is not supported.");
break;
case JpegConstants.Markers.SOS:
@ -428,7 +435,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break;
case JpegConstants.Markers.DAC:
JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with arithmetic coding is not supported.");
if (metadataOnly)
{
stream.Skip(remaining);
}
else
{
this.ProcessArithmeticTable(stream, remaining);
}
break;
}
}
@ -856,6 +871,42 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
private void ProcessArithmeticTable(BufferedReadStream stream, int remaining)
{
this.arithmeticDecodingTables ??= new List<ArithmeticDecodingTable>(4);
while (remaining > 0)
{
int tableClassAndIdentifier = stream.ReadByte();
remaining--;
byte tableClass = (byte)(tableClassAndIdentifier >> 4);
byte identifier = (byte)(tableClassAndIdentifier & 0xF);
byte conditioningTableValue = (byte)stream.ReadByte();
remaining--;
var arithmeticTable = new ArithmeticDecodingTable(tableClass, identifier);
arithmeticTable.Configure(conditioningTableValue);
bool tableEntryReplaced = false;
for (int i = 0; i < this.arithmeticDecodingTables.Count; i++)
{
ArithmeticDecodingTable item = this.arithmeticDecodingTables[i];
if (item.TableClass == arithmeticTable.TableClass && item.Identifier == arithmeticTable.Identifier)
{
this.arithmeticDecodingTables[i] = arithmeticTable;
tableEntryReplaced = true;
break;
}
}
if (!tableEntryReplaced)
{
this.arithmeticDecodingTables.Add(arithmeticTable);
}
}
}
/// <summary>
/// Reads the adobe image resource block name: a Pascal string (padded to make size even).
/// </summary>
@ -917,7 +968,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the tables do not match the header
/// Thrown if the tables do not match the header.
/// </exception>
private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining)
{
@ -1023,8 +1074,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <param name="frameMarker">The current frame marker.</param>
/// <param name="metadataOnly">Whether to parse metadata only</param>
private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
/// <param name="decodingComponentType">The jpeg decoding component type.</param>
/// <param name="metadataOnly">Whether to parse metadata only.</param>
private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly)
{
if (this.Frame != null)
{
@ -1036,17 +1088,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported.");
}
// Read initial marker definitions
// Read initial marker definitions.
const int length = 6;
stream.Read(this.temp, 0, length);
int bytesRead = stream.Read(this.temp, 0, length);
if (bytesRead != length)
{
JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data.");
}
// 1 byte: Bits/sample precision
// 1 byte: Bits/sample precision.
byte precision = this.temp[0];
// Validate: only 8-bit and 12-bit precisions are supported
// Validate: only 8-bit and 12-bit precisions are supported.
if (Array.IndexOf(this.supportedPrecisions, precision) == -1)
{
JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision supported.");
JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision is supported.");
}
// 2 byte: Height
@ -1055,18 +1111,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// 2 byte: Width
int frameWidth = (this.temp[3] << 8) | this.temp[4];
// Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that)
// Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that).
if (frameHeight == 0 || frameWidth == 0)
{
JpegThrowHelper.ThrowInvalidImageDimensions(frameWidth, frameHeight);
}
// 1 byte: Number of components
// 1 byte: Number of components.
byte componentCount = this.temp[5];
// Validate: componentCount more than 4 can lead to a buffer overflow during stream
// reading so we must limit it to 4
// We do not support jpeg images with more than 4 components anyway
// reading so we must limit it to 4.
// We do not support jpeg images with more than 4 components anyway.
if (componentCount > 4)
{
JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount);
@ -1135,7 +1191,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowBadQuantizationTableIndex(quantTableIndex);
}
var component = new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i);
IJpegComponent component = decodingComponentType is ComponentType.Huffman ?
new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i) :
new ArithmeticDecodingComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i);
this.Frame.Components[i] = component;
this.Frame.ComponentIds[i] = componentId;
@ -1165,11 +1223,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
const int codeValuesMaxByteSize = 256;
const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize;
var huffmanScanDecoder = this.scanDecoder as HuffmanScanDecoder;
if (huffmanScanDecoder is null)
{
JpegThrowHelper.ThrowInvalidImageContentException("missing huffman table data");
}
int length = remaining;
using (IMemoryOwner<byte> buffer = this.Configuration.MemoryAllocator.Allocate<byte>(totalBufferSize))
{
Span<byte> bufferSpan = buffer.GetSpan();
Span<byte> huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
Span<byte> huffmanLengthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
Span<byte> huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize);
Span<uint> tableWorkspace = MemoryMarshal.Cast<byte, uint>(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize));
@ -1191,12 +1255,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}.");
}
stream.Read(huffmanLegthsSpan, 1, 16);
stream.Read(huffmanLengthsSpan, 1, 16);
int codeLengthSum = 0;
for (int j = 1; j < 17; j++)
{
codeLengthSum += huffmanLegthsSpan[j];
codeLengthSum += huffmanLengthsSpan[j];
}
length -= 17;
@ -1210,10 +1274,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
i += 17 + codeLengthSum;
this.scanDecoder.BuildHuffmanTable(
huffmanScanDecoder!.BuildHuffmanTable(
tableType,
tableIndex,
huffmanLegthsSpan,
huffmanLengthsSpan,
huffmanValuesSpan.Slice(0, codeLengthSum),
tableWorkspace);
}
@ -1221,8 +1285,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <summary>
/// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in
/// macroblocks
/// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers,
/// in macroblocks.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
@ -1246,7 +1310,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found.");
}
// 1 byte: Number of components in scan
// 1 byte: Number of components in scan.
int selectorsCount = stream.ReadByte();
// Validate: 0 < count <= totalComponents
@ -1256,7 +1320,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException($"Invalid number of components in scan: {selectorsCount}.");
}
// Validate: marker must contain exactly (4 + selectorsCount*2) bytes
// Validate: Marker must contain exactly (4 + selectorsCount*2) bytes
int selectorsBytes = selectorsCount * 2;
if (remaining != 4 + selectorsBytes)
{
@ -1283,7 +1347,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
// Validate: must be found among registered components
// Validate: Must be found among registered components.
if (componentIndex == -1)
{
// TODO: extract as separate method?
@ -1292,7 +1356,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Frame.ComponentOrder[i / 2] = (byte)componentIndex;
JpegComponent component = this.Frame.Components[componentIndex];
IJpegComponent component = this.Frame.Components[componentIndex];
// 1 byte: Huffman table selectors.
// 4 bits - dc
@ -1308,8 +1372,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException($"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}");
}
component.DCHuffmanTableId = dcTableIndex;
component.ACHuffmanTableId = acTableIndex;
component.DcTableId = dcTableIndex;
component.AcTableId = acTableIndex;
}
// 3 bytes: Progressive scan decoding data
@ -1325,18 +1389,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4;
this.scanDecoder.SuccessiveLow = successiveApproximation & 15;
if (this.scanDecoder is ArithmeticScanDecoder arithmeticScanDecoder)
{
arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables);
}
this.scanDecoder.ParseEntropyCodedData(selectorsCount);
}
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <returns>The <see cref="ushort"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
private ushort ReadUint16(BufferedReadStream stream)
{
stream.Read(this.markerBuffer, 0, 2);
int bytesRead = stream.Read(this.markerBuffer, 0, 2);
if (bytesRead != 2)
{
JpegThrowHelper.ThrowInvalidImageContentException("stream does not contain enough data, could not read ushort.");
}
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);
}
}

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

@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
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, scanDecoderGray, CancellationToken.None);
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder?
CopyImageBytesToBuffer(buffer, spectralConverterGray.GetPixelBuffer(CancellationToken.None));
@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
new RgbJpegSpectralConverter<Rgb24>(this.configuration) : new SpectralConverter<Rgb24>(this.configuration);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder?
CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None));

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

@ -40,8 +40,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream);
using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true });
var scanDecoder = new HuffmanScanDecoder(bufferedStream, new NoopSpectralConverter(), cancellationToken: default);
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
var spectralConverter = new NoopSpectralConverter();
decoder.ParseStream(bufferedStream, spectralConverter, cancellationToken: default);
}
// We want to test only stream parsing and scan decoding, we don't need to convert spectral data to actual pixels

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

@ -10,12 +10,10 @@ using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using ImageMagick;
using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests;
using SkiaSharp;
@ -228,8 +226,9 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
public async Task ImageSharpResizeAsync(string input)
{
using FileStream output = File.Open(this.OutputPath(input), FileMode.Create);
// Resize it to fit a 150x150 square
using var image = await ImageSharpImage.LoadAsync(input);
// Resize it to fit a 150x150 square.
using ImageSharpImage image = await ImageSharpImage.LoadAsync(input);
this.LogImageProcessed(image.Width, image.Height);
image.Mutate(i => i.Resize(new ResizeOptions

2
tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs

@ -5,7 +5,6 @@ using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using CommandLine;
@ -68,7 +67,6 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
lrs.leakFrequency = options.LeakFrequency;
lrs.gcFrequency = options.GcFrequency;
timer = Stopwatch.StartNew();
try
{

34
tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs

@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(expectedSizeInBlocks, decoder.Frame.McuSize);
var uniform1 = new Size(1, 1);
JpegComponent c0 = decoder.Components[0];
IJpegComponent c0 = decoder.Components[0];
VerifyJpeg.VerifyComponent(c0, expectedSizeInBlocks, uniform1, uniform1);
}
}
@ -70,8 +70,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
sb.AppendLine(imageFile);
sb.AppendLine($"Size:{decoder.Frame.PixelSize} MCU:{decoder.Frame.McuSize}");
JpegComponent c0 = decoder.Components[0];
JpegComponent c1 = decoder.Components[1];
IJpegComponent c0 = decoder.Components[0];
IJpegComponent c1 = decoder.Components[1];
sb.AppendLine($"Luma: SAMP: {c0.SamplingFactors} BLOCKS: {c0.SizeInBlocks}");
sb.AppendLine($"Chroma: {c1.SamplingFactors} BLOCKS: {c1.SizeInBlocks}");
@ -80,17 +80,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
this.Output.WriteLine(sb.ToString());
}
public static readonly TheoryData<string, int, object, object> ComponentVerificationData = new TheoryData<string, int, object, object>
{
{ TestImages.Jpeg.Baseline.Jpeg444, 3, new Size(1, 1), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Jpeg420Exif, 3, new Size(2, 2), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Jpeg420Small, 3, new Size(2, 2), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Testorig420, 3, new Size(2, 2), new Size(1, 1) },
public static readonly TheoryData<string, int, object, object> ComponentVerificationData = new()
{
{ TestImages.Jpeg.Baseline.Jpeg444, 3, new Size(1, 1), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Jpeg420Exif, 3, new Size(2, 2), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Jpeg420Small, 3, new Size(2, 2), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Testorig420, 3, new Size(2, 2), new Size(1, 1) },
// TODO: Find Ycck or Cmyk images with different subsampling
{ TestImages.Jpeg.Baseline.Ycck, 4, new Size(1, 1), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Cmyk, 4, new Size(1, 1), new Size(1, 1) },
};
// TODO: Find Ycck or Cmyk images with different subsampling
{ TestImages.Jpeg.Baseline.Ycck, 4, new Size(1, 1), new Size(1, 1) },
{ TestImages.Jpeg.Baseline.Cmyk, 4, new Size(1, 1), new Size(1, 1) },
};
[Theory]
[MemberData(nameof(ComponentVerificationData))]
@ -108,9 +108,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(componentCount, decoder.Frame.ComponentCount);
Assert.Equal(componentCount, decoder.Components.Length);
JpegComponent c0 = decoder.Components[0];
JpegComponent c1 = decoder.Components[1];
JpegComponent c2 = decoder.Components[2];
IJpegComponent c0 = decoder.Components[0];
IJpegComponent c1 = decoder.Components[1];
IJpegComponent c2 = decoder.Components[2];
var uniform1 = new Size(1, 1);
@ -126,7 +126,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
if (componentCount == 4)
{
JpegComponent c3 = decoder.Components[2];
IJpegComponent c3 = decoder.Components[2];
VerifyJpeg.VerifyComponent(c3, expectedLumaSizeInBlocks, fLuma, uniform1);
}
}

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

@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default);
// This would parse entire image
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default);
VerifyJpeg.SaveSpectralImage(provider, debugConverter.SpectralData);
}
@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default);
// This would parse entire image
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default);
// Actual verification
this.VerifySpectralCorrectnessImpl(libJpegData, debugConverter.SpectralData);
@ -195,7 +195,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount];
for (int i = 0; i < spectralComponents.Length; i++)
{
JpegComponent component = frame.Components[i];
var component = frame.Components[i] as JpegComponent;
spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index);
}

10
tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Linq;
using System.Threading;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@ -26,10 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Baseline.MultiScanBaselineCMYK
};
public SpectralToPixelConversionTests(ITestOutputHelper output)
{
this.Output = output;
}
public SpectralToPixelConversionTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@ -47,14 +43,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var converter = new SpectralConverter<TPixel>(Configuration.Default);
using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default);
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);
decoder.ParseStream(bufferedStream, converter, cancellationToken: default);
// Test metadata
provider.Utility.TestGroupName = nameof(JpegDecoderTests);
provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName;
// Comparison
using (Image<TPixel> image = new Image<TPixel>(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata()))
using (var image = new Image<TPixel>(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata()))
using (Image<TPixel> referenceImage = provider.GetReferenceOutputImage<TPixel>(appendPixelTypeToFileName: false))
{
ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image);

44
tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs

@ -26,11 +26,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
this.SpectralBlocks = Configuration.Default.MemoryAllocator.Allocate2D<Block8x8>(this.WidthInBlocks, this.HeightInBlocks);
}
public Size Size => new Size(this.WidthInBlocks, this.HeightInBlocks);
public byte Id { get; }
public Size Size => new(this.WidthInBlocks, this.HeightInBlocks);
public int Index { get; }
public Size SizeInBlocks => new Size(this.WidthInBlocks, this.HeightInBlocks);
public Size SizeInBlocks => new(this.WidthInBlocks, this.HeightInBlocks);
public Size SamplingFactors => throw new NotSupportedException();
@ -48,6 +50,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
public short MaxVal { get; private set; } = short.MinValue;
public int HorizontalSamplingFactor => throw new NotImplementedException();
public int VerticalSamplingFactor => throw new NotImplementedException();
public int DcPredictor { get; set; }
public int DcTableId { get; set; }
public int AcTableId { get; set; }
internal void MakeBlock(Block8x8 block, int y, int x)
{
block.TransposeInplace();
@ -77,7 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
}
}
public void LoadSpectral(JpegComponent c)
public void LoadSpectral(IJpegComponent c)
{
Buffer2D<Block8x8> data = c.SpectralBlocks;
for (int y = 0; y < this.HeightInBlocks; y++)
@ -201,25 +213,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
return this.Equals((ComponentData)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(this.Index, this.HeightInBlocks, this.WidthInBlocks, this.MinVal, this.MaxVal);
}
public override int GetHashCode() => HashCode.Combine(this.Index, this.HeightInBlocks, this.WidthInBlocks, this.MinVal, this.MaxVal);
public ref Block8x8 GetBlockReference(int column, int row)
{
throw new NotImplementedException();
}
public ref Block8x8 GetBlockReference(int column, int row) => throw new NotImplementedException();
public static bool operator ==(ComponentData left, ComponentData right)
{
return object.Equals(left, right);
}
public void Init(int maxSubFactorH, int maxSubFactorV) => throw new NotImplementedException();
public static bool operator !=(ComponentData left, ComponentData right)
{
return !object.Equals(left, right);
}
public void AllocateSpectral(bool fullScan) => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
public static bool operator ==(ComponentData left, ComponentData right) => Equals(left, right);
public static bool operator !=(ComponentData left, ComponentData right) => !Equals(left, right);
}
}
}

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

@ -273,7 +273,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[WithFile(Rgba6BitAssociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsOSX)

2
tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs

@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests
this.continueSemaphore.Release();
}
protected override Stream CreateStream() => this.TestFormat.CreateAsyncSamaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable);
protected override Stream CreateStream() => this.TestFormat.CreateAsyncSemaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable);
}
}
}

32
tests/ImageSharp.Tests/TestFormat.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Tests
// We should not change Configuration.Default in individual tests!
// Create new configuration instances with new Configuration(TestFormat.GlobalTestFormat) instead!
public static TestFormat GlobalTestFormat { get; } = new TestFormat();
public static TestFormat GlobalTestFormat { get; } = new();
public TestFormat()
{
@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests
this.Decoder = new TestDecoder(this);
}
public List<DecodeOperation> DecodeCalls { get; } = new List<DecodeOperation>();
public List<DecodeOperation> DecodeCalls { get; } = new();
public TestEncoder Encoder { get; }
@ -54,12 +54,12 @@ namespace SixLabors.ImageSharp.Tests
return ms;
}
public Stream CreateAsyncSamaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512)
public Stream CreateAsyncSemaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512)
{
var buffer = new byte[size];
this.header.CopyTo(buffer, 0);
var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore);
return seeakable ? (Stream)semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false);
return seeakable ? semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false);
}
public void VerifySpecificDecodeCall<TPixel>(byte[] marker, Configuration config)
@ -191,20 +191,14 @@ namespace SixLabors.ImageSharp.Tests
return null;
}
public TestHeader(TestFormat testFormat)
{
this.testFormat = testFormat;
}
public TestHeader(TestFormat testFormat) => this.testFormat = testFormat;
}
public class TestDecoder : IImageDecoder, IImageInfoDetector
{
private readonly TestFormat testFormat;
public TestDecoder(TestFormat testFormat)
{
this.testFormat = testFormat;
}
public TestDecoder(TestFormat testFormat) => this.testFormat = testFormat;
public IEnumerable<string> MimeTypes => new[] { this.testFormat.MimeType };
@ -216,7 +210,6 @@ namespace SixLabors.ImageSharp.Tests
where TPixel : unmanaged, IPixel<TPixel>
=> this.DecodeImpl<TPixel>(configuration, stream);
private Image<TPixel> DecodeImpl<TPixel>(Configuration config, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -246,10 +239,7 @@ namespace SixLabors.ImageSharp.Tests
{
private readonly TestFormat testFormat;
public TestEncoder(TestFormat testFormat)
{
this.testFormat = testFormat;
}
public TestEncoder(TestFormat testFormat) => this.testFormat = testFormat;
public IEnumerable<string> MimeTypes => new[] { this.testFormat.MimeType };
@ -262,16 +252,12 @@ namespace SixLabors.ImageSharp.Tests
}
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO record this happened so we can verify it.
return Task.CompletedTask;
}
where TPixel : unmanaged, IPixel<TPixel> => Task.CompletedTask; // TODO record this happened so we can verify it.
}
public struct TestPixelForAgnosticDecode : IPixel<TestPixelForAgnosticDecode>
{
public PixelOperations<TestPixelForAgnosticDecode> CreatePixelOperations() => new PixelOperations<TestPixelForAgnosticDecode>();
public PixelOperations<TestPixelForAgnosticDecode> CreatePixelOperations() => new();
public void FromScaledVector4(Vector4 vector)
{

Loading…
Cancel
Save