diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index ee0a312803..a22a04980c 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -116,7 +116,7 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options)
///
/// Gets the dimensions of the image.
///
- public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height);
+ public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height);
///
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
@@ -389,7 +389,7 @@ private void ReadRle24(Buffer2D pixels, int width, int height, b
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++)
{
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs b/src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs
new file mode 100644
index 0000000000..ff3c0539c6
--- /dev/null
+++ b/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
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs
new file mode 100644
index 0000000000..a2736900f7
--- /dev/null
+++ b/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)
+ {
+ }
+
+ ///
+ /// Gets or sets the dc context.
+ ///
+ public int DcContext { get; set; }
+
+ ///
+ /// Gets or sets the dc statistics.
+ ///
+ public ArithmeticStatistics DcStatistics { get; set; }
+
+ ///
+ /// Gets or sets the ac statistics.
+ ///
+ public ArithmeticStatistics AcStatistics { get; set; }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs
new file mode 100644
index 0000000000..6055f300d5
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
new file mode 100644
index 0000000000..d3a5ea15b0
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
@@ -0,0 +1,1238 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using SixLabors.ImageSharp.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
+{
+ ///
+ /// Decodes a arithmetic encoded spectral scan.
+ /// Based on https://github.com/yigolden/JpegLibrary/blob/main/src/JpegLibrary/ScanDecoder/JpegArithmeticScanDecoder.cs
+ ///
+ internal class ArithmeticScanDecoder : IJpegScanDecoder
+ {
+ private readonly BufferedReadStream stream;
+
+ private int c;
+ private int a;
+ private int ct;
+
+ ///
+ /// instance containing decoding-related information.
+ ///
+ private JpegFrame frame;
+
+ ///
+ /// Shortcut for .Components.
+ ///
+ private IJpegComponent[] components;
+
+ ///
+ /// Number of component in the current scan.
+ ///
+ private int scanComponentCount;
+
+ ///
+ /// The reset interval determined by RST markers.
+ ///
+ private int restartInterval;
+
+ ///
+ /// How many mcu's are left to do.
+ ///
+ private int todo;
+
+ private readonly SpectralConverter spectralConverter;
+
+ private JpegBitReader scanBuffer;
+
+ private ArithmeticDecodingTable[] dcDecodingTables;
+
+ private ArithmeticDecodingTable[] acDecodingTables;
+
+ private readonly byte[] fixedBin = { 113, 0, 0, 0 };
+
+ private readonly CancellationToken cancellationToken;
+
+ private static readonly int[] ArithmeticTable =
+ {
+ Pack(0x5a1d, 1, 1, 1),
+ Pack(0x2586, 14, 2, 0),
+ Pack(0x1114, 16, 3, 0),
+ Pack(0x080b, 18, 4, 0),
+ Pack(0x03d8, 20, 5, 0),
+ Pack(0x01da, 23, 6, 0),
+ Pack(0x00e5, 25, 7, 0),
+ Pack(0x006f, 28, 8, 0),
+ Pack(0x0036, 30, 9, 0),
+ Pack(0x001a, 33, 10, 0),
+ Pack(0x000d, 35, 11, 0),
+ Pack(0x0006, 9, 12, 0),
+ Pack(0x0003, 10, 13, 0),
+ Pack(0x0001, 12, 13, 0),
+ Pack(0x5a7f, 15, 15, 1),
+ Pack(0x3f25, 36, 16, 0),
+ Pack(0x2cf2, 38, 17, 0),
+ Pack(0x207c, 39, 18, 0),
+ Pack(0x17b9, 40, 19, 0),
+ Pack(0x1182, 42, 20, 0),
+ Pack(0x0cef, 43, 21, 0),
+ Pack(0x09a1, 45, 22, 0),
+ Pack(0x072f, 46, 23, 0),
+ Pack(0x055c, 48, 24, 0),
+ Pack(0x0406, 49, 25, 0),
+ Pack(0x0303, 51, 26, 0),
+ Pack(0x0240, 52, 27, 0),
+ Pack(0x01b1, 54, 28, 0),
+ Pack(0x0144, 56, 29, 0),
+ Pack(0x00f5, 57, 30, 0),
+ Pack(0x00b7, 59, 31, 0),
+ Pack(0x008a, 60, 32, 0),
+ Pack(0x0068, 62, 33, 0),
+ Pack(0x004e, 63, 34, 0),
+ Pack(0x003b, 32, 35, 0),
+ Pack(0x002c, 33, 9, 0),
+ Pack(0x5ae1, 37, 37, 1),
+ Pack(0x484c, 64, 38, 0),
+ Pack(0x3a0d, 65, 39, 0),
+ Pack(0x2ef1, 67, 40, 0),
+ Pack(0x261f, 68, 41, 0),
+ Pack(0x1f33, 69, 42, 0),
+ Pack(0x19a8, 70, 43, 0),
+ Pack(0x1518, 72, 44, 0),
+ Pack(0x1177, 73, 45, 0),
+ Pack(0x0e74, 74, 46, 0),
+ Pack(0x0bfb, 75, 47, 0),
+ Pack(0x09f8, 77, 48, 0),
+ Pack(0x0861, 78, 49, 0),
+ Pack(0x0706, 79, 50, 0),
+ Pack(0x05cd, 48, 51, 0),
+ Pack(0x04de, 50, 52, 0),
+ Pack(0x040f, 50, 53, 0),
+ Pack(0x0363, 51, 54, 0),
+ Pack(0x02d4, 52, 55, 0),
+ Pack(0x025c, 53, 56, 0),
+ Pack(0x01f8, 54, 57, 0),
+ Pack(0x01a4, 55, 58, 0),
+ Pack(0x0160, 56, 59, 0),
+ Pack(0x0125, 57, 60, 0),
+ Pack(0x00f6, 58, 61, 0),
+ Pack(0x00cb, 59, 62, 0),
+ Pack(0x00ab, 61, 63, 0),
+ Pack(0x008f, 61, 32, 0),
+ Pack(0x5b12, 65, 65, 1),
+ Pack(0x4d04, 80, 66, 0),
+ Pack(0x412c, 81, 67, 0),
+ Pack(0x37d8, 82, 68, 0),
+ Pack(0x2fe8, 83, 69, 0),
+ Pack(0x293c, 84, 70, 0),
+ Pack(0x2379, 86, 71, 0),
+ Pack(0x1edf, 87, 72, 0),
+ Pack(0x1aa9, 87, 73, 0),
+ Pack(0x174e, 72, 74, 0),
+ Pack(0x1424, 72, 75, 0),
+ Pack(0x119c, 74, 76, 0),
+ Pack(0x0f6b, 74, 77, 0),
+ Pack(0x0d51, 75, 78, 0),
+ Pack(0x0bb6, 77, 79, 0),
+ Pack(0x0a40, 77, 48, 0),
+ Pack(0x5832, 80, 81, 1),
+ Pack(0x4d1c, 88, 82, 0),
+ Pack(0x438e, 89, 83, 0),
+ Pack(0x3bdd, 90, 84, 0),
+ Pack(0x34ee, 91, 85, 0),
+ Pack(0x2eae, 92, 86, 0),
+ Pack(0x299a, 93, 87, 0),
+ Pack(0x2516, 86, 71, 0),
+ Pack(0x5570, 88, 89, 1),
+ Pack(0x4ca9, 95, 90, 0),
+ Pack(0x44d9, 96, 91, 0),
+ Pack(0x3e22, 97, 92, 0),
+ Pack(0x3824, 99, 93, 0),
+ Pack(0x32b4, 99, 94, 0),
+ Pack(0x2e17, 93, 86, 0),
+ Pack(0x56a8, 95, 96, 1),
+ Pack(0x4f46, 101, 97, 0),
+ Pack(0x47e5, 102, 98, 0),
+ Pack(0x41cf, 103, 99, 0),
+ Pack(0x3c3d, 104, 100, 0),
+ Pack(0x375e, 99, 93, 0),
+ Pack(0x5231, 105, 102, 0),
+ Pack(0x4c0f, 106, 103, 0),
+ Pack(0x4639, 107, 104, 0),
+ Pack(0x415e, 103, 99, 0),
+ Pack(0x5627, 105, 106, 1),
+ Pack(0x50e7, 108, 107, 0),
+ Pack(0x4b85, 109, 103, 0),
+ Pack(0x5597, 110, 109, 0),
+ Pack(0x504f, 111, 107, 0),
+ Pack(0x5a10, 110, 111, 1),
+ Pack(0x5522, 112, 109, 0),
+ Pack(0x59eb, 112, 111, 1),
+
+ // This last entry is used for fixed probability estimate of 0.5
+ // as suggested in Section 10.3 Table 5 of ITU-T Rec. T.851.
+ Pack(0x5a1d, 113, 113, 0)
+ };
+
+ private readonly List statistics = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The input stream.
+ /// Spectral to pixel converter.
+ /// The token to monitor cancellation.
+ public ArithmeticScanDecoder(BufferedReadStream stream, SpectralConverter converter, CancellationToken cancellationToken)
+ {
+ this.stream = stream;
+ this.spectralConverter = converter;
+ this.cancellationToken = cancellationToken;
+
+ this.c = 0;
+ this.a = 0;
+ this.ct = -16; // Force reading 2 initial bytes to fill C.
+ }
+
+ ///
+ public int ResetInterval
+ {
+ set
+ {
+ this.restartInterval = value;
+ this.todo = value;
+ }
+ }
+
+ ///
+ public int SpectralStart { get; set; }
+
+ ///
+ public int SpectralEnd { get; set; }
+
+ ///
+ public int SuccessiveHigh { get; set; }
+
+ ///
+ public int SuccessiveLow { get; set; }
+
+ public void InitDecodingTables(List arithmeticDecodingTables)
+ {
+ for (int i = 0; i < this.components.Length; i++)
+ {
+ var component = this.components[i] as ArithmeticDecodingComponent;
+ this.dcDecodingTables[i] = this.GetArithmeticTable(arithmeticDecodingTables, true, component.DcTableId);
+ component.DcStatistics = this.CreateOrGetStatisticsBin(true, component.DcTableId);
+ this.acDecodingTables[i] = this.GetArithmeticTable(arithmeticDecodingTables, false, component.AcTableId);
+ component.AcStatistics = this.CreateOrGetStatisticsBin(false, component.AcTableId);
+ }
+ }
+
+ private ref byte GetFixedBinReference() => ref this.fixedBin[0];
+
+ ///
+ /// Decodes the entropy coded data.
+ ///
+ /// Component count in the current scan.
+ public void ParseEntropyCodedData(int scanComponentCount)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+
+ this.scanComponentCount = scanComponentCount;
+
+ this.scanBuffer = new JpegBitReader(this.stream);
+
+ bool fullScan = this.frame.Progressive || this.frame.MultiScan;
+ this.frame.AllocateComponents(fullScan);
+
+ if (this.frame.Progressive)
+ {
+ this.ParseProgressiveData();
+ }
+ else
+ {
+ this.ParseBaselineData();
+ }
+
+ if (this.scanBuffer.HasBadMarker())
+ {
+ this.stream.Position = this.scanBuffer.MarkerPosition;
+ }
+ }
+
+ ///
+ public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
+ {
+ this.frame = frame;
+ this.components = frame.Components;
+
+ this.dcDecodingTables = new ArithmeticDecodingTable[this.components.Length];
+ this.acDecodingTables = new ArithmeticDecodingTable[this.components.Length];
+
+ this.spectralConverter.InjectFrameData(frame, jpegData);
+ }
+
+ private ArithmeticDecodingTable GetArithmeticTable(List arithmeticDecodingTables, bool isDcTable, int identifier)
+ {
+ int tableClass = isDcTable ? 0 : 1;
+
+ foreach (ArithmeticDecodingTable item in arithmeticDecodingTables)
+ {
+ if (item.TableClass == tableClass && item.Identifier == identifier)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ private ArithmeticStatistics CreateOrGetStatisticsBin(bool dc, int identifier, bool reset = false)
+ {
+ foreach (ArithmeticStatistics item in this.statistics)
+ {
+ if (item.IsDcStatistics == dc && item.Identifier == identifier)
+ {
+ if (reset)
+ {
+ item.Reset();
+ }
+
+ return item;
+ }
+ }
+
+ var statistic = new ArithmeticStatistics(dc, identifier);
+ this.statistics.Add(statistic);
+ return statistic;
+ }
+
+ private void ParseBaselineData()
+ {
+ foreach (ArithmeticDecodingComponent component in this.components)
+ {
+ component.DcPredictor = 0;
+ component.DcContext = 0;
+ component.DcStatistics?.Reset();
+ component.AcStatistics?.Reset();
+ }
+
+ this.Reset();
+
+ if (this.scanComponentCount != 1)
+ {
+ this.ParseBaselineDataInterleaved();
+ this.spectralConverter.CommitConversion();
+ }
+ else if (this.frame.ComponentCount == 1)
+ {
+ this.ParseBaselineDataSingleComponent();
+ this.spectralConverter.CommitConversion();
+ }
+ else
+ {
+ this.ParseBaselineDataNonInterleaved();
+ }
+ }
+
+ private void ParseProgressiveData()
+ {
+ this.CheckProgressiveData();
+
+ foreach (ArithmeticDecodingComponent component in this.components)
+ {
+ if (this.SpectralStart == 0 && this.SuccessiveHigh == 0)
+ {
+ component.DcPredictor = 0;
+ component.DcContext = 0;
+ component.DcStatistics?.Reset();
+ }
+
+ if (this.SpectralStart != 0)
+ {
+ component.AcStatistics?.Reset();
+ }
+ }
+
+ this.Reset();
+
+ if (this.scanComponentCount == 1)
+ {
+ this.ParseProgressiveDataNonInterleaved();
+ }
+ else
+ {
+ this.ParseProgressiveDataInterleaved();
+ }
+ }
+
+ private void CheckProgressiveData()
+ {
+ // Validate successive scan parameters.
+ // Logic has been adapted from libjpeg.
+ // See Table B.3 – Scan header parameter size and values. itu-t81.pdf
+ bool invalid = false;
+ if (this.SpectralStart == 0)
+ {
+ if (this.SpectralEnd != 0)
+ {
+ invalid = true;
+ }
+ }
+ else
+ {
+ // Need not check Ss/Se < 0 since they came from unsigned bytes.
+ if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63)
+ {
+ invalid = true;
+ }
+
+ // AC scans may have only one component.
+ if (this.scanComponentCount != 1)
+ {
+ invalid = true;
+ }
+ }
+
+ if (this.SuccessiveHigh != 0)
+ {
+ // Successive approximation refinement scan: must have Al = Ah-1.
+ if (this.SuccessiveHigh - 1 != this.SuccessiveLow)
+ {
+ invalid = true;
+ }
+ }
+
+ // TODO: How does this affect 12bit jpegs.
+ // According to libjpeg the range covers 8bit only?
+ if (this.SuccessiveLow > 13)
+ {
+ invalid = true;
+ }
+
+ if (invalid)
+ {
+ JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow);
+ }
+ }
+
+ private void ParseBaselineDataInterleaved()
+ {
+ int mcu = 0;
+ int mcusPerColumn = this.frame.McusPerColumn;
+ int mcusPerLine = this.frame.McusPerLine;
+ ref JpegBitReader reader = ref this.scanBuffer;
+
+ for (int j = 0; j < mcusPerColumn; j++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+
+ // Decode from binary to spectral.
+ for (int i = 0; i < mcusPerLine; i++)
+ {
+ // Scan an interleaved mcu... process components in order.
+ int mcuCol = mcu % mcusPerLine;
+ for (int k = 0; k < this.scanComponentCount; k++)
+ {
+ int order = this.frame.ComponentOrder[k];
+ var component = this.components[order] as ArithmeticDecodingComponent;
+
+ ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId];
+ ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId];
+
+ int h = component.HorizontalSamplingFactor;
+ int v = component.VerticalSamplingFactor;
+
+ // Scan out an mcu's worth of this component; that's just determined
+ // by the basic H and V specified for the component.
+ int mcuColMulh = mcuCol * h;
+ for (int y = 0; y < v; y++)
+ {
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int x = 0; x < h; x++)
+ {
+ if (reader.NoData)
+ {
+ // It is very likely that some spectral data was decoded before we've encountered 'end of scan'
+ // so we need to decode what's left and return (or maybe throw?)
+ this.spectralConverter.ConvertStrideBaseline();
+ return;
+ }
+
+ int blockCol = mcuColMulh + x;
+
+ this.DecodeBlockBaseline(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)blockCol),
+ ref acDecodingTable,
+ ref dcDecodingTable);
+ }
+ }
+ }
+
+ // After all interleaved components, that's an interleaved MCU,
+ // so now count down the restart interval.
+ mcu++;
+ this.HandleRestart();
+ }
+
+ // Convert from spectral to actual pixels via given converter.
+ this.spectralConverter.ConvertStrideBaseline();
+ }
+ }
+
+ private void ParseBaselineDataSingleComponent()
+ {
+ var component = this.frame.Components[0] as ArithmeticDecodingComponent;
+ int mcuLines = this.frame.McusPerColumn;
+ int w = component.WidthInBlocks;
+ int h = component.SamplingFactors.Height;
+ ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId];
+ ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId];
+
+ ref JpegBitReader reader = ref this.scanBuffer;
+
+ for (int i = 0; i < mcuLines; i++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+
+ // Decode from binary to spectral.
+ for (int j = 0; j < h; j++)
+ {
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int k = 0; k < w; k++)
+ {
+ if (reader.NoData)
+ {
+ // It is very likely that some spectral data was decoded before we've encountered 'end of scan'
+ // so we need to decode what's left and return (or maybe throw?)
+ this.spectralConverter.ConvertStrideBaseline();
+ return;
+ }
+
+ this.DecodeBlockBaseline(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)k),
+ ref acDecodingTable,
+ ref dcDecodingTable);
+
+ this.HandleRestart();
+ }
+ }
+
+ // Convert from spectral to actual pixels via given converter.
+ this.spectralConverter.ConvertStrideBaseline();
+ }
+ }
+
+ private void ParseBaselineDataNonInterleaved()
+ {
+ var component = (ArithmeticDecodingComponent)this.components[this.frame.ComponentOrder[0]];
+ ref JpegBitReader reader = ref this.scanBuffer;
+
+ int w = component.WidthInBlocks;
+ int h = component.HeightInBlocks;
+
+ ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId];
+ ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId];
+
+ for (int j = 0; j < h; j++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int i = 0; i < w; i++)
+ {
+ if (reader.NoData)
+ {
+ return;
+ }
+
+ this.DecodeBlockBaseline(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)i),
+ ref acDecodingTable,
+ ref dcDecodingTable);
+
+ this.HandleRestart();
+ }
+ }
+ }
+
+ private void ParseProgressiveDataInterleaved()
+ {
+ int mcu = 0;
+ int mcusPerColumn = this.frame.McusPerColumn;
+ int mcusPerLine = this.frame.McusPerLine;
+ ref JpegBitReader reader = ref this.scanBuffer;
+
+ for (int j = 0; j < mcusPerColumn; j++)
+ {
+ for (int i = 0; i < mcusPerLine; i++)
+ {
+ // Scan an interleaved mcu... process components in order.
+ int mcuRow = Math.DivRem(mcu, mcusPerLine, out int mcuCol);
+ for (int k = 0; k < this.scanComponentCount; k++)
+ {
+ int order = this.frame.ComponentOrder[k];
+ var component = this.components[order] as ArithmeticDecodingComponent;
+ ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId];
+
+ int h = component.HorizontalSamplingFactor;
+ int v = component.VerticalSamplingFactor;
+
+ // Scan out an mcu's worth of this component; that's just determined
+ // by the basic H and V specified for the component.
+ int mcuColMulh = mcuCol * h;
+ for (int y = 0; y < v; y++)
+ {
+ int blockRow = (mcuRow * v) + y;
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(blockRow);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int x = 0; x < h; x++)
+ {
+ if (reader.NoData)
+ {
+ return;
+ }
+
+ int blockCol = mcuColMulh + x;
+
+ this.DecodeBlockProgressiveDc(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)blockCol),
+ ref dcDecodingTable);
+ }
+ }
+ }
+
+ // After all interleaved components, that's an interleaved MCU,
+ // so now count down the restart interval.
+ mcu++;
+ this.HandleRestart();
+ }
+ }
+ }
+
+ private void ParseProgressiveDataNonInterleaved()
+ {
+ var component = this.components[this.frame.ComponentOrder[0]] as ArithmeticDecodingComponent;
+ ref JpegBitReader reader = ref this.scanBuffer;
+
+ int w = component.WidthInBlocks;
+ int h = component.HeightInBlocks;
+
+ if (this.SpectralStart == 0)
+ {
+ ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId];
+
+ for (int j = 0; j < h; j++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int i = 0; i < w; i++)
+ {
+ if (reader.NoData)
+ {
+ return;
+ }
+
+ this.DecodeBlockProgressiveDc(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)i),
+ ref dcDecodingTable);
+
+ this.HandleRestart();
+ }
+ }
+ }
+ else
+ {
+ ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId];
+
+ for (int j = 0; j < h; j++)
+ {
+ this.cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int i = 0; i < w; i++)
+ {
+ if (reader.NoData)
+ {
+ return;
+ }
+
+ this.DecodeBlockProgressiveAc(
+ component,
+ ref Unsafe.Add(ref blockRef, (nint)(uint)i),
+ ref acDecodingTable);
+
+ this.HandleRestart();
+ }
+ }
+ }
+ }
+
+ private void DecodeBlockProgressiveDc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable dcTable)
+ {
+ if (dcTable == null)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing");
+ }
+
+ ref JpegBitReader reader = ref this.scanBuffer;
+ ref short blockDataRef = ref Unsafe.As(ref block);
+
+ if (this.SuccessiveHigh == 0)
+ {
+ // First scan
+ // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients.
+
+ // Table F.4: Point to statistics bin S0 for DC coefficient coding.
+ ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), component.DcContext);
+
+ // Figure F.19: Decode_DC_DIFF
+ if (this.DecodeBinaryDecision(ref reader, ref st) == 0)
+ {
+ component.DcContext = 0;
+ }
+ else
+ {
+ // Figure F.21: Decoding nonzero value v.
+ // Figure F.22: Decoding the sign of v.
+ int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1));
+ st = ref Unsafe.Add(ref st, (nint)(uint)(2 + sign));
+
+ // Figure F.23: Decoding the magnitude category of v.
+ int m = this.DecodeBinaryDecision(ref reader, ref st);
+ if (m != 0)
+ {
+ st = ref component.DcStatistics.GetReference(20);
+ while (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ if ((m <<= 1) == 0x8000)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+
+ st = ref Unsafe.Add(ref st, 1);
+ }
+ }
+
+ // Section F.1.4.4.1.2: Establish dc_context conditioning category.
+ if (m < (int)((1L << dcTable.DcL) >> 1))
+ {
+ component.DcContext = 0; // Zero diff category.
+ }
+ else if (m > (int)((1L << dcTable.DcU) >> 1))
+ {
+ component.DcContext = 12 + (sign * 4); // Large diff category.
+ }
+ else
+ {
+ component.DcContext = 4 + (sign * 4); // Small diff category.
+ }
+
+ int v = m;
+
+ // Figure F.24: Decoding the magnitude bit pattern of v.
+ st = ref Unsafe.Add(ref st, 14);
+ while ((m >>= 1) != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ v |= m;
+ }
+ }
+
+ v += 1;
+ if (sign != 0)
+ {
+ v = -v;
+ }
+
+ component.DcPredictor = (short)(component.DcPredictor + v);
+ }
+
+ blockDataRef = (short)(component.DcPredictor << this.SuccessiveLow);
+ }
+ else
+ {
+ // Refinement scan.
+ ref byte st = ref this.GetFixedBinReference();
+
+ blockDataRef |= (short)(this.DecodeBinaryDecision(ref reader, ref st) << this.SuccessiveLow);
+ }
+ }
+
+ private void DecodeBlockProgressiveAc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable acTable)
+ {
+ ref JpegBitReader reader = ref this.scanBuffer;
+ ref short blockDataRef = ref Unsafe.As(ref block);
+
+ ArithmeticStatistics acStatistics = component.AcStatistics;
+ if (acStatistics == null || acTable == null)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing");
+ }
+
+ if (this.SuccessiveHigh == 0)
+ {
+ // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients.
+
+ // Figure F.20: Decode_AC_coefficients.
+ int start = this.SpectralStart;
+ int end = this.SpectralEnd;
+ int low = this.SuccessiveLow;
+
+ for (int k = start; k <= end; k++)
+ {
+ ref byte st = ref acStatistics.GetReference(3 * (k - 1));
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ break;
+ }
+
+ while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0)
+ {
+ st = ref Unsafe.Add(ref st, 3);
+ k++;
+ if (k > 63)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+ }
+
+ // Figure F.21: Decoding nonzero value v.
+ // Figure F.22: Decoding the sign of v.
+ int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference());
+ st = ref Unsafe.Add(ref st, 2);
+
+ // Figure F.23: Decoding the magnitude category of v.
+ int m = this.DecodeBinaryDecision(ref reader, ref st);
+ if (m != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ m <<= 1;
+ st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217);
+ while (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ if ((m <<= 1) == 0x8000)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+
+ st = ref Unsafe.Add(ref st, 1);
+ }
+ }
+ }
+
+ int v = m;
+
+ // Figure F.24: Decoding the magnitude bit pattern of v.
+ st = ref Unsafe.Add(ref st, 14);
+ while ((m >>= 1) != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ v |= m;
+ }
+ }
+
+ v += 1;
+ if (sign != 0)
+ {
+ v = -v;
+ }
+
+ Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]) = (short)(v << low);
+ }
+ }
+ else
+ {
+ // Refinement scan.
+ this.ReadBlockProgressiveAcRefined(acStatistics, ref blockDataRef);
+ }
+ }
+
+ private void ReadBlockProgressiveAcRefined(ArithmeticStatistics acStatistics, ref short blockDataRef)
+ {
+ ref JpegBitReader reader = ref this.scanBuffer;
+ int start = this.SpectralStart;
+ int end = this.SpectralEnd;
+
+ int p1 = 1 << this.SuccessiveLow;
+ int m1 = -1 << this.SuccessiveLow;
+
+ // Establish EOBx (previous stage end-of-block) index.
+ int kex = end;
+ for (; kex > 0; kex--)
+ {
+ if (Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[kex]) != 0)
+ {
+ break;
+ }
+ }
+
+ for (int k = start; k <= end; k++)
+ {
+ ref byte st = ref acStatistics.GetReference(3 * (k - 1));
+ if (k > kex)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ break;
+ }
+ }
+
+ while (true)
+ {
+ ref short coef = ref Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]);
+ if (coef != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 2)) != 0)
+ {
+ coef = (short)(coef + (coef < 0 ? m1 : p1));
+ }
+
+ break;
+ }
+
+ if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) != 0)
+ {
+ bool flag = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()) != 0;
+ coef = (short)(coef + (flag ? m1 : p1));
+
+ break;
+ }
+
+ st = ref Unsafe.Add(ref st, 3);
+ k++;
+ if (k > end)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+ }
+ }
+ }
+
+ private void DecodeBlockBaseline(
+ ArithmeticDecodingComponent component,
+ ref Block8x8 destinationBlock,
+ ref ArithmeticDecodingTable acTable,
+ ref ArithmeticDecodingTable dcTable)
+ {
+ if (acTable is null)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing.");
+ }
+
+ if (dcTable is null)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing.");
+ }
+
+ ref JpegBitReader reader = ref this.scanBuffer;
+ ref short destinationRef = ref Unsafe.As(ref destinationBlock);
+
+ // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients.
+
+ // Table F.4: Point to statistics bin S0 for DC coefficient coding.
+ ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), component.DcContext);
+
+ /* Figure F.19: Decode_DC_DIFF */
+ if (this.DecodeBinaryDecision(ref reader, ref st) == 0)
+ {
+ component.DcContext = 0;
+ }
+ else
+ {
+ // Figure F.21: Decoding nonzero value v
+ // Figure F.22: Decoding the sign of v
+ int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1));
+ st = ref Unsafe.Add(ref st, (nint)(uint)(2 + sign));
+
+ // Figure F.23: Decoding the magnitude category of v.
+ int m = this.DecodeBinaryDecision(ref reader, ref st);
+ if (m != 0)
+ {
+ // Table F.4: X1 = 20
+ st = ref component.DcStatistics.GetReference(20);
+ while (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ if ((m <<= 1) == 0x8000)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+
+ st = ref Unsafe.Add(ref st, 1);
+ }
+ }
+
+ // Section F.1.4.4.1.2: Establish dc_context conditioning category.
+ if (m < (int)((1L << dcTable.DcL) >> 1))
+ {
+ component.DcContext = 0; // zero diff category
+ }
+ else if (m > (int)((1L << dcTable.DcU) >> 1))
+ {
+ component.DcContext = 12 + (sign * 4); // large diff category
+ }
+ else
+ {
+ component.DcContext = 4 + (sign * 4); // small diff category
+ }
+
+ int v = m;
+
+ // Figure F.24: Decoding the magnitude bit pattern of v.
+ st = ref Unsafe.Add(ref st, 14);
+ while ((m >>= 1) != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ v |= m;
+ }
+ }
+
+ v += 1;
+ if (sign != 0)
+ {
+ v = -v;
+ }
+
+ component.DcPredictor = (short)(component.DcPredictor + v);
+ }
+
+ destinationRef = (short)component.DcPredictor;
+
+ // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients.
+ ArithmeticStatistics acStatistics = component.AcStatistics;
+
+ for (int k = 1; k <= 63; k++)
+ {
+ st = ref acStatistics.GetReference(3 * (k - 1));
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ // EOB flag.
+ break;
+ }
+
+ while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0)
+ {
+ st = ref Unsafe.Add(ref st, 3);
+ k++;
+ if (k > 63)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+ }
+
+ // Figure F.21: Decoding nonzero value v.
+ // Figure F.22: Decoding the sign of v.
+ int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference());
+ st = ref Unsafe.Add(ref st, 2);
+
+ // Figure F.23: Decoding the magnitude category of v.
+ int m = this.DecodeBinaryDecision(ref reader, ref st);
+ if (m != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ m <<= 1;
+ st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217);
+ while (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ if ((m <<= 1) == 0x8000)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code.");
+ }
+
+ st = ref Unsafe.Add(ref st, 1);
+ }
+ }
+ }
+
+ int v = m;
+
+ // Figure F.24: Decoding the magnitude bit pattern of v.
+ st = ref Unsafe.Add(ref st, 14);
+ while ((m >>= 1) != 0)
+ {
+ if (this.DecodeBinaryDecision(ref reader, ref st) != 0)
+ {
+ v |= m;
+ }
+ }
+
+ v += 1;
+ if (sign != 0)
+ {
+ v = -v;
+ }
+
+ Unsafe.Add(ref destinationRef, ZigZag.TransposingOrder[k]) = (short)v;
+ }
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private bool HandleRestart()
+ {
+ if (this.restartInterval > 0 && (--this.todo) == 0)
+ {
+ if (this.scanBuffer.Marker == JpegConstants.Markers.XFF)
+ {
+ if (!this.scanBuffer.FindNextMarker())
+ {
+ return false;
+ }
+ }
+
+ this.todo = this.restartInterval;
+
+ foreach (ArithmeticDecodingComponent component in this.components)
+ {
+ component.DcPredictor = 0;
+ component.DcContext = 0;
+ component.DcStatistics?.Reset();
+ component.AcStatistics?.Reset();
+ }
+
+ this.Reset();
+
+ if (this.scanBuffer.HasRestartMarker())
+ {
+ this.Reset();
+ return true;
+ }
+
+ if (this.scanBuffer.HasBadMarker())
+ {
+ this.stream.Position = this.scanBuffer.MarkerPosition;
+ this.Reset();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private void Reset()
+ {
+ for (int i = 0; i < this.components.Length; i++)
+ {
+ var component = this.components[i] as ArithmeticDecodingComponent;
+ component.DcPredictor = 0;
+ }
+
+ this.c = 0;
+ this.a = 0;
+ this.ct = -16; // Force reading 2 initial bytes to fill C.
+
+ this.scanBuffer.Reset();
+ }
+
+ private int DecodeBinaryDecision(ref JpegBitReader reader, ref byte st)
+ {
+ // Renormalization & data input per section D.2.6
+ while (this.a < 0x8000)
+ {
+ if (--this.ct < 0)
+ {
+ // Need to fetch next data byte.
+ reader.CheckBits();
+ int data = reader.GetBits(8);
+
+ // Insert data into C register.
+ this.c = (this.c << 8) | data;
+
+ // Update bit shift counter.
+ if ((this.ct += 8) < 0)
+ {
+ // Need more initial bytes.
+ if (++this.ct == 0)
+ {
+ // Got 2 initial bytes -> re-init A and exit loop
+ this.a = 0x8000; // e->a = 0x10000L after loop exit
+ }
+ }
+ }
+
+ this.a <<= 1;
+ }
+
+ // Fetch values from our compact representation of Table D.3(D.2):
+ // Qe values and probability estimation state machine
+ int sv = st;
+ int qe = ArithmeticTable[sv & 0x7f];
+ byte nl = (byte)qe;
+ qe >>= 8; // Next_Index_LPS + Switch_MPS
+ byte nm = (byte)qe;
+ qe >>= 8; // Next_Index_MPS
+
+ // Decode & estimation procedures per sections D.2.4 & D.2.5
+ int temp = this.a - qe;
+ this.a = temp;
+ temp <<= this.ct;
+ if (this.c >= temp)
+ {
+ this.c -= temp;
+
+ // Conditional LPS (less probable symbol) exchange
+ if (this.a < qe)
+ {
+ this.a = qe;
+ st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS
+ }
+ else
+ {
+ this.a = qe;
+ st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS
+ sv ^= 0x80; // Exchange LPS/MPS
+ }
+ }
+ else if (this.a < 0x8000)
+ {
+ // Conditional MPS (more probable symbol) exchange
+ if (this.a < qe)
+ {
+ st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS
+ sv ^= 0x80; // Exchange LPS/MPS
+ }
+ else
+ {
+ st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS
+ }
+ }
+
+ return sv >> 7;
+ }
+
+ // The following function specifies the packing of the four components
+ // into the compact INT32 representation.
+ // Note that this formula must match the actual arithmetic encoder and decoder implementation. The implementation has to be changed
+ // if this formula is changed.
+ // The current organization is leaned on Markus Kuhn's JBIG implementation (jbig_tab.c).
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int Pack(int a, int b, int c, int d)
+ => (a << 16) | (c << 8) | (d << 7) | b;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs
new file mode 100644
index 0000000000..c84831b3aa
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index e1faf93f49..da2d5da65a 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Originally ported from
/// with additional fixes for both performance and common encoding errors.
///
- internal class HuffmanScanDecoder
+ internal class HuffmanScanDecoder : IJpegScanDecoder
{
private readonly BufferedReadStream stream;
@@ -26,7 +26,7 @@ internal class HuffmanScanDecoder
///
/// Shortcut for .Components.
///
- private JpegComponent[] components;
+ private IJpegComponent[] components;
///
/// Number of component in the current scan.
@@ -54,11 +54,11 @@ internal class HuffmanScanDecoder
private readonly HuffmanTable[] dcHuffmanTables;
///
- /// The AC Huffman tables
+ /// The AC Huffman tables.
///
private readonly HuffmanTable[] acHuffmanTables;
- private HuffmanScanBuffer scanBuffer;
+ private JpegBitReader scanBuffer;
private readonly SpectralConverter spectralConverter;
@@ -119,7 +119,7 @@ public void ParseEntropyCodedData(int scanComponentCount)
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 @@ public void ParseEntropyCodedData(int scanComponentCount)
}
}
+ ///
public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
this.frame = frame;
@@ -170,7 +171,7 @@ private void ParseBaselineDataInterleaved()
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 @@ private void ParseBaselineDataInterleaved()
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 @@ ref Unsafe.Add(ref blockRef, blockCol),
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 @@ ref Unsafe.Add(ref blockRef, i),
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 @@ private void ParseProgressiveDataInterleaved()
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 @@ private void ParseProgressiveDataInterleaved()
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 @@ ref Unsafe.Add(ref blockRef, blockCol),
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 @@ ref Unsafe.Add(ref blockRef, i),
}
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 @@ private void DecodeBlockBaseline(
ref HuffmanTable acTable)
{
ref short blockDataRef = ref Unsafe.As(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 @@ private void DecodeBlockBaseline(
private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 block, ref HuffmanTable dcTable)
{
ref short blockDataRef = ref Unsafe.As(ref block);
- ref HuffmanScanBuffer buffer = ref this.scanBuffer;
+ ref JpegBitReader buffer = ref this.scanBuffer;
if (this.SuccessiveHigh == 0)
{
@@ -581,7 +582,7 @@ private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTab
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 @@ private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTab
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;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
index bee5e0229b..79713388b2 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
@@ -25,7 +25,7 @@ internal unsafe struct HuffmanTable
///
/// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to
- /// ensure terminates.
+ /// ensure terminates.
///
public fixed ulong MaxCode[18];
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs
index 54077339d1..adab8c2ec3 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs
@@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
internal interface IJpegComponent
{
+ ///
+ /// Gets the component id.
+ ///
+ byte Id { get; }
+
///
/// Gets the component's position in the components array.
///
@@ -25,6 +30,16 @@ internal interface IJpegComponent
///
Size SamplingFactors { get; }
+ ///
+ /// Gets the horizontal sampling factor.
+ ///
+ int HorizontalSamplingFactor { get; }
+
+ ///
+ /// Gets the vertical sampling factor.
+ ///
+ int VerticalSamplingFactor { get; }
+
///
/// Gets the divisors needed to apply when calculating colors.
///
@@ -44,5 +59,38 @@ internal interface IJpegComponent
/// We need to apply IDCT and dequantization to transform them into color-space blocks.
///
Buffer2D SpectralBlocks { get; }
+
+ ///
+ /// Gets or sets DC coefficient predictor.
+ ///
+ int DcPredictor { get; set; }
+
+ ///
+ /// Gets or sets the index for the DC table.
+ ///
+ int DcTableId { get; set; }
+
+ ///
+ /// Gets or sets the index for the AC table.
+ ///
+ int AcTableId { get; set; }
+
+ ///
+ /// Initializes component for future buffers initialization.
+ ///
+ /// Maximal horizontal subsampling factor among all the components.
+ /// Maximal vertical subsampling factor among all the components.
+ void Init(int maxSubFactorH, int maxSubFactorV);
+
+ ///
+ /// Allocates the spectral blocks.
+ ///
+ /// if set to true, use the full height of a block, otherwise use the vertical sampling factor.
+ void AllocateSpectral(bool fullScan);
+
+ ///
+ /// Releases resources.
+ ///
+ void Dispose();
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs
new file mode 100644
index 0000000000..73ca9f08b2
--- /dev/null
+++ b/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
+{
+ ///
+ /// Interface for a JPEG scan decoder.
+ ///
+ internal interface IJpegScanDecoder
+ {
+ ///
+ /// Sets the reset interval.
+ ///
+ int ResetInterval { set; }
+
+ ///
+ /// Gets or sets the spectral selection start.
+ ///
+ int SpectralStart { get; set; }
+
+ ///
+ /// Gets or sets the spectral selection end.
+ ///
+ int SpectralEnd { get; set; }
+
+ ///
+ /// Gets or sets the successive approximation high bit end.
+ ///
+ int SuccessiveHigh { get; set; }
+
+ ///
+ /// Gets or sets the successive approximation low bit end.
+ ///
+ int SuccessiveLow { get; set; }
+
+ ///
+ /// Decodes the entropy coded data.
+ ///
+ /// Component count in the current scan.
+ void ParseEntropyCodedData(int scanComponentCount);
+
+ ///
+ /// Sets the JpegFrame and its components and injects the frame data into the spectral converter.
+ ///
+ /// The frame.
+ /// The raw JPEG data.
+ void InjectFrameData(JpegFrame frame, IRawJpegData jpegData);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
index 33815e539c..dd7ca4e7ff 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs
@@ -18,7 +18,7 @@ internal interface IRawJpegData : IDisposable
///
/// Gets the components.
///
- IJpegComponent[] Components { get; }
+ JpegComponent[] Components { get; }
///
/// Gets the quantization tables, in natural order.
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
similarity index 98%
rename from src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
rename to src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
index 3664cb4eb3..84013319e1 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
@@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
/// Used to buffer and track the bits read from the Huffman entropy encoded data.
///
- internal struct HuffmanScanBuffer
+ internal struct JpegBitReader
{
private readonly BufferedReadStream stream;
@@ -22,7 +22,7 @@ internal struct HuffmanScanBuffer
// 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;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
index 3804e1c6c0..5b0e877853 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs
@@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
/// Represents a single frame component.
///
- internal sealed class JpegComponent : IDisposable, IJpegComponent
+ internal class JpegComponent : IDisposable, IJpegComponent
{
private readonly MemoryAllocator memoryAllocator;
@@ -78,12 +78,12 @@ public JpegComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id,
///
/// Gets or sets the index for the DC Huffman table.
///
- public int DCHuffmanTableId { get; set; }
+ public int DcTableId { get; set; }
///
/// Gets or sets the index for the AC Huffman table.
///
- public int ACHuffmanTableId { get; set; }
+ public int AcTableId { get; set; }
public JpegFrame Frame { get; }
@@ -119,11 +119,12 @@ public void Init(int maxSubFactorH, int maxSubFactorV)
}
}
+ ///
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;
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
index c3bf1cbdd5..6173c4fbf8 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs
@@ -103,7 +103,7 @@ public void CopyBlocksToColorBuffer(int spectralStep)
// 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],
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
index fc109be261..db1febd399 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
@@ -6,14 +6,14 @@
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
///
- /// Represent a single jpeg frame
+ /// Represent a single jpeg frame.
///
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 @@ public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height
///
/// Gets the pixel size of the image.
///
- public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight);
+ public Size PixelSize => new(this.PixelWidth, this.PixelHeight);
///
/// Gets the number of components within a frame.
@@ -101,7 +101,7 @@ public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height
///
/// Gets the mcu size of the image.
///
- public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn);
+ public Size McuSize => new(this.McusPerLine, this.McusPerColumn);
///
/// Gets the color depth, in number of bits per pixel.
@@ -134,7 +134,7 @@ public void Init(int maxSubFactorH, int maxSubFactorV)
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 @@ public void AllocateComponents(bool fullScan)
{
for (int i = 0; i < this.ComponentCount; i++)
{
- JpegComponent component = this.Components[i];
+ IJpegComponent component = this.Components[i];
component.AllocateSpectral(fullScan);
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/JpegConstants.cs
index 89c4de5500..20edf40379 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegConstants.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegConstants.cs
@@ -306,17 +306,17 @@ internal static class Huffman
public const int RegisterSize = 64;
///
- /// The number of bits to fetch when filling the buffer.
+ /// The number of bits to fetch when filling the buffer.
///
public const int FetchBits = 48;
///
- /// The number of times to read the input stream when filling the buffer.
+ /// The number of times to read the input stream when filling the buffer.
///
public const int FetchLoop = FetchBits / 8;
///
- /// The minimum number of bits allowed before by the before fetching.
+ /// The minimum number of bits allowed before by the before fetching.
///
public const int MinBits = RegisterSize - FetchBits;
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 533ffa719c..a07db1c952 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/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,17 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
///
/// Scan decoder.
///
- private HuffmanScanDecoder scanDecoder;
+ private IJpegScanDecoder scanDecoder;
+
+ ///
+ /// The arithmetic decoding tables.
+ ///
+ private List arithmeticDecodingTables;
+
+ ///
+ /// The restart interval.
+ ///
+ private int? resetInterval;
///
/// Initializes a new instance of the class.
@@ -140,7 +151,7 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options)
public JpegComponent[] Components => this.Frame.Components;
///
- IJpegComponent[] IRawJpegData.Components => this.Components;
+ JpegComponent[] IRawJpegData.Components => this.Components;
///
public Block8x8F[] QuantizationTables { get; private set; }
@@ -188,9 +199,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken
{
using var spectralConverter = new SpectralConverter(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 +215,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken
///
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,13 +231,12 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
/// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips).
///
/// The table bytes.
- /// The scan decoder.
- public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder)
+ /// The scan decoder.
+ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder)
{
this.Metadata = new ImageMetadata();
this.QuantizationTables = new Block8x8F[4];
- this.scanDecoder = huffmanScanDecoder;
-
+ this.scanDecoder = scanDecoder;
if (tableBytes.Length < 4)
{
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker");
@@ -300,13 +308,13 @@ public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder)
/// Parses the input stream for file markers.
///
/// The input stream.
- /// Scan decoder used exclusively to decode SOS marker.
+ /// The spectral converter to use.
/// The token to monitor cancellation.
- internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDecoder, CancellationToken cancellationToken)
+ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralConverter, CancellationToken cancellationToken)
{
- bool metadataOnly = scanDecoder == null;
+ bool metadataOnly = spectralConverter == null;
- this.scanDecoder = scanDecoder;
+ this.scanDecoder ??= new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
this.Metadata = new ImageMetadata();
@@ -335,9 +343,9 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco
// Get the marker length.
int markerContentByteSize = this.ReadUint16(stream) - 2;
- // Check whether stream actually has enought bytes to read
+ // Check whether stream actually has enough bytes to read
// markerContentByteSize is always positive so we cast
- // to uint to avoid sign extension
+ // to uint to avoid sign extension.
if (stream.RemainingBytes < (uint)markerContentByteSize)
{
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
@@ -348,7 +356,21 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco
case JpegConstants.Markers.SOF0:
case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2:
- this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, metadataOnly);
+
+ this.ProcessStartOfFrameMarker(stream, markerContentByteSize, 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);
+ if (this.resetInterval.HasValue)
+ {
+ this.scanDecoder.ResetInterval = this.resetInterval.Value;
+ }
+
+ this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly);
break;
case JpegConstants.Markers.SOF5:
@@ -364,13 +386,9 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco
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:
@@ -454,7 +472,15 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco
break;
case JpegConstants.Markers.DAC:
- JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with arithmetic coding is not supported.");
+ if (metadataOnly)
+ {
+ stream.Skip(markerContentByteSize);
+ }
+ else
+ {
+ this.ProcessArithmeticTable(stream, markerContentByteSize);
+ }
+
break;
}
}
@@ -889,6 +915,47 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining)
}
}
+ ///
+ /// Processes a DAC marker, decoding the arithmetic tables.
+ ///
+ /// The input stream.
+ /// The remaining bytes in the segment block.
+ private void ProcessArithmeticTable(BufferedReadStream stream, int remaining)
+ {
+ this.arithmeticDecodingTables ??= new List(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);
+ }
+ }
+ }
+
///
/// Reads the adobe image resource block name: a Pascal string (padded to make size even).
///
@@ -950,7 +1017,7 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining)
/// The input stream.
/// The remaining bytes in the segment block.
///
- /// Thrown if the tables do not match the header
+ /// Thrown if the tables do not match the header.
///
private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining)
{
@@ -1056,8 +1123,9 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in
/// The input stream.
/// The remaining bytes in the segment block.
/// The current frame marker.
- /// Whether to parse metadata only
- private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly)
+ /// The jpeg decoding component type.
+ /// Whether to parse metadata only.
+ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly)
{
if (this.Frame != null)
{
@@ -1069,17 +1137,21 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining,
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
@@ -1088,18 +1160,18 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining,
// 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);
@@ -1168,9 +1240,11 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining,
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.Components[i] = (JpegComponent)component;
this.Frame.ComponentIds[i] = componentId;
index += componentBytes;
@@ -1198,11 +1272,17 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem
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 buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize))
{
Span bufferSpan = buffer.GetSpan();
- Span huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
+ Span huffmanLengthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize);
Span tableWorkspace = MemoryMarshal.Cast(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize));
@@ -1224,12 +1304,12 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem
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;
@@ -1243,10 +1323,10 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem
i += 17 + codeLengthSum;
- this.scanDecoder.BuildHuffmanTable(
+ huffmanScanDecoder!.BuildHuffmanTable(
tableType,
tableIndex,
- huffmanLegthsSpan,
+ huffmanLengthsSpan,
huffmanValuesSpan.Slice(0, codeLengthSum),
tableWorkspace);
}
@@ -1254,8 +1334,8 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem
}
///
- /// 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.
///
/// The input stream.
/// The remaining bytes in the segment block.
@@ -1266,7 +1346,14 @@ private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int r
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining);
}
- this.scanDecoder.ResetInterval = this.ReadUint16(stream);
+ // Save the reset interval, because it can come before or after the SOF marker.
+ // If the reset interval comes after the SOF marker, the scanDecoder has not been created.
+ this.resetInterval = this.ReadUint16(stream);
+
+ if (this.scanDecoder != null)
+ {
+ this.scanDecoder.ResetInterval = this.resetInterval.Value;
+ }
}
///
@@ -1279,7 +1366,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
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
@@ -1289,7 +1376,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
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)
{
@@ -1316,7 +1403,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
}
}
- // Validate: must be found among registered components
+ // Validate: Must be found among registered components.
if (componentIndex == -1)
{
// TODO: extract as separate method?
@@ -1325,7 +1412,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
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
@@ -1341,8 +1428,8 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
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.
@@ -1362,11 +1449,16 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4;
this.scanDecoder.SuccessiveLow = successiveApproximation & 15;
+ if (this.scanDecoder is ArithmeticScanDecoder arithmeticScanDecoder)
+ {
+ arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables);
+ }
+
this.scanDecoder.ParseEntropyCodedData(selectorsCount);
}
///
- /// Reads a from the stream advancing it by two bytes
+ /// Reads a from the stream advancing it by two bytes.
///
/// The input stream.
/// The
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index 4e788c76af..88dbcb8828 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -62,7 +62,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(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?
using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
@@ -79,7 +79,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(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?
using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
index 988c056608..450c786adc 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
@@ -40,8 +40,8 @@ public void ParseStream()
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
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
index 81a95cd1ee..58da1f8d5e 100644
--- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
@@ -10,12 +10,10 @@
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 @@ public void ImageSharpResize(string input)
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
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
index 0c7b157b2b..c5f3124609 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
@@ -67,7 +67,6 @@ public static void Run(string[] args)
lrs.leakFrequency = options.LeakFrequency;
lrs.gcFrequency = options.GcFrequency;
-
timer = Stopwatch.StartNew();
try
{
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs
index 021e3d2726..9aaf2b968f 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs
@@ -3,7 +3,7 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
-
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
// ReSharper disable InconsistentNaming
@@ -49,6 +49,20 @@ static void RunTest(string providerDump, string nonContiguousBuffersStr)
// .Dispose();
}
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCoding01, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCoding02, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingGray, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingInterleaved, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingWithRestart, PixelTypes.Rgb24)]
+ public void DecodeJpeg_WithArithmeticCoding(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(JpegDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Tolerant(0.002f), ReferenceDecoder);
+ }
+
[Theory]
[WithFileCollection(nameof(UnrecoverableTestJpegs), PixelTypes.Rgba32)]
public void UnrecoverableImage_Throws_InvalidImageContentException(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
index d5e0f081bf..543619c876 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
@@ -72,10 +72,6 @@ public partial class JpegDecoderTests
TestImages.Jpeg.Issues.Fuzz.NullReferenceException823,
TestImages.Jpeg.Issues.MalformedUnsupportedComponentCount,
- // Arithmetic coding
- TestImages.Jpeg.Baseline.ArithmeticCoding,
- TestImages.Jpeg.Baseline.ArithmeticCodingProgressive,
-
// Lossless jpeg
TestImages.Jpeg.Baseline.Lossless
};
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs
index e8533b9bca..30e5da4ef5 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs
@@ -4,6 +4,7 @@
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
// ReSharper disable InconsistentNaming
@@ -29,6 +30,17 @@ public void DecodeProgressiveJpeg(TestImageProvider provider)
appendPixelTypeToFileName: false);
}
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingProgressive01, PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingProgressive02, PixelTypes.Rgb24)]
+ public void DecodeProgressiveJpeg_WithArithmeticCoding(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(JpegDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Tolerant(0.004f), ReferenceDecoder);
+ }
+
[Theory]
[WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgb24)]
public void DecodeProgressiveJpeg_WithLimitedAllocatorBufferCapacity(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 1faa6f0f4c..e39aaa323e 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -13,7 +13,7 @@
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;
@@ -25,6 +25,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[ValidateDisposedMemoryAllocations]
public partial class JpegDecoderTests
{
+ private static MagickReferenceDecoder ReferenceDecoder => new();
+
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector;
private const float BaselineTolerance = 0.001F / 100;
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
index c4d0faf33d..714d993009 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs
@@ -50,7 +50,7 @@ public void ComponentScalingIsCorrect_1ChannelJpeg()
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 @@ public void PrintComponentData(string imageFile)
{
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 @@ public void PrintComponentData(string imageFile)
this.Output.WriteLine(sb.ToString());
}
- public static readonly TheoryData ComponentVerificationData = new TheoryData
- {
- { 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 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 @@ public void ComponentScalingIsCorrect_MultiChannelJpeg(
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 @@ public void ComponentScalingIsCorrect_MultiChannelJpeg(
if (componentCount == 4)
{
- JpegComponent c3 = decoder.Components[2];
+ IJpegComponent c3 = decoder.Components[2];
VerifyJpeg.VerifyComponent(c3, expectedLumaSizeInBlocks, fLuma, uniform1);
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
index 3833b419c4..c4a448ff8e 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
@@ -59,7 +59,7 @@ public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider(TestImageProvider provider
// internal scan decoder which we substitute to assert spectral correctness
var debugConverter = new DebugSpectralConverter();
- 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 +194,7 @@ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
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);
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
index 27240831c3..99a31a9725 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs
+++ b/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 @@ public class SpectralToPixelConversionTests
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 @@ public void Decoder_PixelBufferComparison(TestImageProvider prov
using var converter = new SpectralConverter(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 image = new Image(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata()))
+ using (var image = new Image(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata()))
using (Image referenceImage = provider.GetReferenceOutputImage(appendPixelTypeToFileName: false))
{
ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
index a390212d15..f3335fe305 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs
@@ -26,11 +26,13 @@ public ComponentData(int widthInBlocks, int heightInBlocks, int index)
this.SpectralBlocks = Configuration.Default.MemoryAllocator.Allocate2D(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 @@ public ComponentData(int widthInBlocks, int heightInBlocks, int index)
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 @@ public void LoadSpectralStride(Buffer2D data, int strideIndex)
}
}
- public void LoadSpectral(JpegComponent c)
+ public void LoadSpectral(IJpegComponent c)
{
Buffer2D data = c.SpectralBlocks;
for (int y = 0; y < this.HeightInBlocks; y++)
@@ -201,25 +213,19 @@ public override bool Equals(object obj)
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);
}
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 9460f3a351..ceded79cc2 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -274,7 +274,7 @@ public void TiffDecoder_CanDecode_24Bit_WithUnassociatedAlpha(TestImageP
[Theory]
[WithFile(Rgba6BitAssociatedAlpha, PixelTypes.Rgba32)]
- public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha(TestImageProvider provider)
+ public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
if (TestEnvironment.IsMacOS)
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
index 317a5129c4..8319138445 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
@@ -124,7 +124,7 @@ private async Task DoCancel()
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);
}
}
}
diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs
index efe9585521..32da06901e 100644
--- a/tests/ImageSharp.Tests/TestFormat.cs
+++ b/tests/ImageSharp.Tests/TestFormat.cs
@@ -24,7 +24,7 @@ public class TestFormat : IConfigurationModule, IImageFormat
// 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 @@ public TestFormat()
this.Decoder = new TestDecoder(this);
}
- public List DecodeCalls { get; } = new List();
+ public List DecodeCalls { get; } = new();
public TestEncoder Encoder { get; }
@@ -54,12 +54,12 @@ public MemoryStream CreateStream(byte[] marker = null)
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(byte[] marker, Configuration config)
@@ -191,20 +191,14 @@ public IImageFormat DetectFormat(ReadOnlySpan header)
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 MimeTypes => new[] { this.testFormat.MimeType };
@@ -216,7 +210,6 @@ public Image Decode(Configuration configuration, Stream stream,
where TPixel : unmanaged, IPixel
=> this.DecodeImpl(configuration, stream);
-
private Image DecodeImpl(Configuration config, Stream stream)
where TPixel : unmanaged, IPixel
{
@@ -246,10 +239,7 @@ public class TestEncoder : IImageEncoder
{
private readonly TestFormat testFormat;
- public TestEncoder(TestFormat testFormat)
- {
- this.testFormat = testFormat;
- }
+ public TestEncoder(TestFormat testFormat) => this.testFormat = testFormat;
public IEnumerable MimeTypes => new[] { this.testFormat.MimeType };
@@ -262,16 +252,12 @@ public void Encode(Image image, Stream stream)
}
public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken)
- where TPixel : unmanaged, IPixel
- {
- // TODO record this happened so we can verify it.
- return Task.CompletedTask;
- }
+ where TPixel : unmanaged, IPixel => Task.CompletedTask; // TODO record this happened so we can verify it.
}
public struct TestPixelForAgnosticDecode : IPixel
{
- public PixelOperations CreatePixelOperations() => new PixelOperations();
+ public PixelOperations CreatePixelOperations() => new();
public void FromScaledVector4(Vector4 vector)
{
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 310056fb35..fa51fb2254 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -214,14 +214,21 @@ public static class Bad
public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg";
public const string HistogramEqImage = "Jpg/baseline/640px-Unequalized_Hawkes_Bay_NZ.jpg";
public const string ForestBridgeDifferentComponentsQuality = "Jpg/baseline/forest_bridge.jpg";
- public const string ArithmeticCoding = "Jpg/baseline/arithmetic_coding.jpg";
- public const string ArithmeticCodingProgressive = "Jpg/progressive/arithmetic_progressive.jpg";
public const string Lossless = "Jpg/baseline/lossless.jpg";
public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg";
public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg";
public const string ExtendedXmp = "Jpg/baseline/extended-xmp.jpg";
public const string GrayscaleSampling2x2 = "Jpg/baseline/grayscale_sampling22.jpg";
+ // Jpeg's with arithmetic coding.
+ public const string ArithmeticCoding01 = "Jpg/baseline/Calliphora_arithmetic.jpg";
+ public const string ArithmeticCoding02 = "Jpg/baseline/arithmetic_coding.jpg";
+ public const string ArithmeticCodingProgressive01 = "Jpg/progressive/arithmetic_progressive.jpg";
+ public const string ArithmeticCodingProgressive02 = "Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg";
+ public const string ArithmeticCodingGray = "Jpg/baseline/Calliphora-arithmetic-grayscale.jpg";
+ public const string ArithmeticCodingInterleaved = "Jpg/baseline/Calliphora-arithmetic-interleaved.jpg";
+ public const string ArithmeticCodingWithRestart = "Jpg/baseline/Calliphora-arithmetic-restart.jpg";
+
public static readonly string[] All =
{
Cmyk, Ycck, Exif, Floorplan,
diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg
new file mode 100644
index 0000000000..59256be594
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5800857969f25773606ecb63154a7fee60b831f13932dc845e50276bac11b16
+size 177311
diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg
new file mode 100644
index 0000000000..9dc93473da
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09bb5d75c3ca9d92d6e6489611f1d9b9815aaec70a16027f4ca17e371aa69e6e
+size 234032
diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg
new file mode 100644
index 0000000000..5ba56de1ab
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad547d0de50d1d623a49dc7794838c6d35372ea3fe4bda06365ff8b42daf65bf
+size 234225
diff --git a/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg b/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg
new file mode 100644
index 0000000000..9dc93473da
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09bb5d75c3ca9d92d6e6489611f1d9b9815aaec70a16027f4ca17e371aa69e6e
+size 234032
diff --git a/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg b/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg
new file mode 100644
index 0000000000..91879221c4
--- /dev/null
+++ b/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd688e22840892d7fd511782f3ff7043df1a3131fb17b000c6a3ca1d0e069950
+size 228527