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