diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index db65b84cca..ba5c588ca5 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -879,5 +879,13 @@ ref MemoryMarshal.GetReference(Log2DeBruijn), (IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here } #endif + + /// + /// Fast division with ceiling for numbers. + /// + /// Divident value. + /// Divisor value. + /// Ceiled division result. + public static uint DivideCeil(uint value, uint divisor) => (value + divisor - 1) / divisor; } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 6424ee23ac..a09c7ada3e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -16,29 +16,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal class HuffmanScanDecoder { - private readonly JpegFrame frame; - private readonly HuffmanTable[] dcHuffmanTables; - private readonly HuffmanTable[] acHuffmanTables; private readonly BufferedReadStream stream; - private readonly JpegComponent[] components; - // The restart interval. - private readonly int restartInterval; - - // The number of interleaved components. - private readonly int componentsLength; - - // The spectral selection start. - private readonly int spectralStart; - - // The spectral selection end. - private readonly int spectralEnd; - - // The successive approximation high bit end. - private readonly int successiveHigh; + // Frame related + private JpegFrame frame; + private JpegComponent[] components; - // The successive approximation low bit end. - private readonly int successiveLow; + // The restart interval. + private int restartInterval; // How many mcu's are left to do. private int todo; @@ -51,52 +36,57 @@ internal class HuffmanScanDecoder private HuffmanScanBuffer scanBuffer; + private readonly SpectralConverter spectralConverter; + private CancellationToken cancellationToken; /// /// Initializes a new instance of the class. /// /// The input stream. - /// The image frame. - /// The DC Huffman tables. - /// The AC Huffman tables. - /// The length of the components. Different to the array length. - /// The reset interval. - /// The spectral selection start. - /// The spectral selection end. - /// The successive approximation bit high end. - /// The successive approximation bit low end. + /// Spectral to pixel converter. /// The token to monitor cancellation. public HuffmanScanDecoder( BufferedReadStream stream, - JpegFrame frame, - HuffmanTable[] dcHuffmanTables, - HuffmanTable[] acHuffmanTables, - int componentsLength, - int restartInterval, - int spectralStart, - int spectralEnd, - int successiveHigh, - int successiveLow, + SpectralConverter converter, CancellationToken cancellationToken) { this.dctZigZag = ZigZag.CreateUnzigTable(); this.stream = stream; - this.scanBuffer = new HuffmanScanBuffer(stream); - this.frame = frame; - this.dcHuffmanTables = dcHuffmanTables; - this.acHuffmanTables = acHuffmanTables; - this.components = frame.Components; - this.componentsLength = componentsLength; - this.restartInterval = restartInterval; - this.todo = restartInterval; - this.spectralStart = spectralStart; - this.spectralEnd = spectralEnd; - this.successiveHigh = successiveHigh; - this.successiveLow = successiveLow; + this.spectralConverter = converter; this.cancellationToken = cancellationToken; } + // huffman tables + public HuffmanTable[] DcHuffmanTables { get; set; } + + public HuffmanTable[] AcHuffmanTables { get; set; } + + // Reset interval + public int ResetInterval + { + set + { + this.restartInterval = value; + this.todo = value; + } + } + + // The number of interleaved components. + public int ComponentsLength { get; set; } + + // The spectral selection start. + public int SpectralStart { get; set; } + + // The spectral selection end. + public int SpectralEnd { get; set; } + + // The successive approximation high bit end. + public int SuccessiveHigh { get; set; } + + // The successive approximation low bit end. + public int SuccessiveLow { get; set; } + /// /// Decodes the entropy coded data. /// @@ -104,6 +94,11 @@ public void ParseEntropyCodedData() { this.cancellationToken.ThrowIfCancellationRequested(); + this.scanBuffer = new HuffmanScanBuffer(this.stream); + + bool fullScan = this.frame.Progressive || this.frame.MultiScan; + this.frame.AllocateComponents(fullScan); + if (!this.frame.Progressive) { this.ParseBaselineData(); @@ -119,15 +114,23 @@ public void ParseEntropyCodedData() } } + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.components = frame.Components; + + this.spectralConverter.InjectFrameData(frame, jpegData); + } + private void ParseBaselineData() { - if (this.componentsLength == 1) + if (this.ComponentsLength == this.frame.ComponentCount) { - this.ParseBaselineDataNonInterleaved(); + this.ParseBaselineDataInterleaved(); } else { - this.ParseBaselineDataInterleaved(); + this.ParseBaselineDataNonInterleaved(); } } @@ -140,13 +143,13 @@ private void ParseBaselineDataInterleaved() ref HuffmanScanBuffer buffer = ref this.scanBuffer; // Pre-derive the huffman table to avoid in-loop checks. - for (int i = 0; i < this.componentsLength; i++) + for (int i = 0; i < this.ComponentsLength; i++) { int order = this.frame.ComponentOrder[i]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; dcHuffmanTable.Configure(); acHuffmanTable.Configure(); } @@ -155,18 +158,18 @@ private void ParseBaselineDataInterleaved() { this.cancellationToken.ThrowIfCancellationRequested(); + // decode from binary to spectral for (int i = 0; i < mcusPerLine; i++) { // Scan an interleaved mcu... process components in order - int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.componentsLength; k++) + for (int k = 0; k < this.ComponentsLength; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -175,14 +178,16 @@ private void ParseBaselineDataInterleaved() // by the basic H and V specified for the component for (int y = 0; y < v; y++) { - int blockRow = (mcuRow * v) + y; - Span blockSpan = component.SpectralBlocks.GetRowSpan(blockRow); + Span blockSpan = component.SpectralBlocks.GetRowSpan(y); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int x = 0; x < h; x++) { if (buffer.NoData) { + // It is very likely that some spectral data was decoded before we encountered EOI marker + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(); return; } @@ -202,6 +207,9 @@ ref Unsafe.Add(ref blockRef, blockCol), mcu++; this.HandleRestart(); } + + // convert from spectral to actual pixels via given converter + this.spectralConverter.ConvertStrideBaseline(); } } @@ -213,8 +221,8 @@ private void ParseBaselineDataNonInterleaved() 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.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; dcHuffmanTable.Configure(); acHuffmanTable.Configure(); @@ -248,9 +256,9 @@ private void CheckProgressiveData() // 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.SpectralStart == 0) { - if (this.spectralEnd != 0) + if (this.SpectralEnd != 0) { invalid = true; } @@ -258,22 +266,22 @@ private void CheckProgressiveData() else { // Need not check Ss/Se < 0 since they came from unsigned bytes. - if (this.spectralEnd < this.spectralStart || this.spectralEnd > 63) + if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63) { invalid = true; } // AC scans may have only one component. - if (this.componentsLength != 1) + if (this.ComponentsLength != 1) { invalid = true; } } - if (this.successiveHigh != 0) + if (this.SuccessiveHigh != 0) { // Successive approximation refinement scan: must have Al = Ah-1. - if (this.successiveHigh - 1 != this.successiveLow) + if (this.SuccessiveHigh - 1 != this.SuccessiveLow) { invalid = true; } @@ -281,14 +289,14 @@ private void CheckProgressiveData() // TODO: How does this affect 12bit jpegs. // According to libjpeg the range covers 8bit only? - if (this.successiveLow > 13) + if (this.SuccessiveLow > 13) { invalid = true; } if (invalid) { - JpegThrowHelper.ThrowBadProgressiveScan(this.spectralStart, this.spectralEnd, this.successiveHigh, this.successiveLow); + JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow); } } @@ -296,7 +304,7 @@ private void ParseProgressiveData() { this.CheckProgressiveData(); - if (this.componentsLength == 1) + if (this.ComponentsLength == 1) { this.ParseProgressiveDataNonInterleaved(); } @@ -315,11 +323,11 @@ private void ParseProgressiveDataInterleaved() ref HuffmanScanBuffer buffer = ref this.scanBuffer; // Pre-derive the huffman table to avoid in-loop checks. - for (int k = 0; k < this.componentsLength; k++) + for (int k = 0; k < this.ComponentsLength; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; dcHuffmanTable.Configure(); } @@ -330,11 +338,11 @@ private void ParseProgressiveDataInterleaved() // Scan an interleaved mcu... process components in order int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.componentsLength; k++) + for (int k = 0; k < this.ComponentsLength; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -380,9 +388,9 @@ private void ParseProgressiveDataNonInterleaved() int w = component.WidthInBlocks; int h = component.HeightInBlocks; - if (this.spectralStart == 0) + if (this.SpectralStart == 0) { - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; dcHuffmanTable.Configure(); for (int j = 0; j < h; j++) @@ -410,7 +418,7 @@ ref Unsafe.Add(ref blockRef, i), } else { - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; acHuffmanTable.Configure(); for (int j = 0; j < h; j++) @@ -489,7 +497,7 @@ private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 bloc ref short blockDataRef = ref Unsafe.As(ref block); ref HuffmanScanBuffer buffer = ref this.scanBuffer; - if (this.successiveHigh == 0) + if (this.SuccessiveHigh == 0) { // First scan for DC coefficient, must be first int s = buffer.DecodeHuffman(ref dcTable); @@ -500,20 +508,20 @@ private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 bloc s += component.DcPredictor; component.DcPredictor = s; - blockDataRef = (short)(s << this.successiveLow); + blockDataRef = (short)(s << this.SuccessiveLow); } else { // Refinement scan for DC coefficient buffer.CheckBits(); - blockDataRef |= (short)(buffer.GetBits(1) << this.successiveLow); + blockDataRef |= (short)(buffer.GetBits(1) << this.SuccessiveLow); } } private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTable) { ref short blockDataRef = ref Unsafe.As(ref block); - if (this.successiveHigh == 0) + if (this.SuccessiveHigh == 0) { // MCU decoding for AC initial scan (either spectral selection, // or first pass of successive approximation). @@ -525,9 +533,9 @@ private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTab ref HuffmanScanBuffer buffer = ref this.scanBuffer; ref ZigZag zigzag = ref this.dctZigZag; - int start = this.spectralStart; - int end = this.spectralEnd; - int low = this.successiveLow; + int start = this.SpectralStart; + int end = this.SpectralEnd; + int low = this.SuccessiveLow; for (int i = start; i <= end; ++i) { @@ -571,11 +579,11 @@ private void DecodeBlockProgressiveACRefined(ref short blockDataRef, ref Huffman // Refinement scan for these AC coefficients ref HuffmanScanBuffer buffer = ref this.scanBuffer; ref ZigZag zigzag = ref this.dctZigZag; - int start = this.spectralStart; - int end = this.spectralEnd; + int start = this.SpectralStart; + int end = this.SpectralEnd; - int p1 = 1 << this.successiveLow; - int m1 = (-1) << this.successiveLow; + int p1 = 1 << this.SuccessiveLow; + int m1 = (-1) << this.SuccessiveLow; int k = start; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index 5c3ee6e28e..614e96e54a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -109,10 +109,10 @@ public void Dispose() public void Init() { this.WidthInBlocks = (int)MathF.Ceiling( - MathF.Ceiling(this.Frame.SamplesPerLine / 8F) * this.HorizontalSamplingFactor / this.Frame.MaxHorizontalFactor); + MathF.Ceiling(this.Frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / this.Frame.MaxHorizontalFactor); this.HeightInBlocks = (int)MathF.Ceiling( - MathF.Ceiling(this.Frame.Scanlines / 8F) * this.VerticalSamplingFactor / this.Frame.MaxVerticalFactor); + MathF.Ceiling(this.Frame.PixelHeight / 8F) * this.VerticalSamplingFactor / this.Frame.MaxVerticalFactor); int blocksPerLineForMcu = this.Frame.McusPerLine * this.HorizontalSamplingFactor; int blocksPerColumnForMcu = this.Frame.McusPerColumn * this.VerticalSamplingFactor; @@ -125,12 +125,20 @@ public void Init() { JpegThrowHelper.ThrowBadSampling(); } + } + + public void AllocateSpectral(bool fullScan) + { + if (this.SpectralBlocks != null) + { + // this method will be called each scan marker so we need to allocate only once + return; + } - int totalNumberOfBlocks = blocksPerColumnForMcu * (blocksPerLineForMcu + 1); - int width = this.WidthInBlocks + 1; - int height = totalNumberOfBlocks / width; + int spectralAllocWidth = this.SizeInBlocks.Width; + int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor; - this.SpectralBlocks = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean); + this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index fc1ebaf921..79965a3f0c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -2,15 +2,12 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { /// - /// Encapsulates postprocessing data for one component for . + /// Encapsulates spectral data to rgba32 processing for one component. /// internal class JpegComponentPostProcessor : IDisposable { @@ -27,23 +24,20 @@ internal class JpegComponentPostProcessor : IDisposable /// /// Initializes a new instance of the class. /// - public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegImagePostProcessor imagePostProcessor, IJpegComponent component) + public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) { this.Component = component; - this.ImagePostProcessor = imagePostProcessor; + this.RawJpeg = rawJpeg; this.blockAreaSize = this.Component.SubSamplingDivisors * 8; this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( - imagePostProcessor.PostProcessorBufferSize.Width, - imagePostProcessor.PostProcessorBufferSize.Height, + postProcessorBufferSize.Width, + postProcessorBufferSize.Height, this.blockAreaSize.Height); - this.BlockRowsPerStep = JpegImagePostProcessor.BlockRowsPerStep / this.Component.SubSamplingDivisors.Height; + this.BlockRowsPerStep = postProcessorBufferSize.Height / 8 / this.Component.SubSamplingDivisors.Height; } - /// - /// Gets the - /// - public JpegImagePostProcessor ImagePostProcessor { get; } + public IRawJpegData RawJpeg { get; } /// /// Gets the @@ -66,26 +60,27 @@ public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegImagePost public int BlockRowsPerStep { get; } /// - public void Dispose() - { - this.ColorBuffer.Dispose(); - } + public void Dispose() => this.ColorBuffer.Dispose(); /// /// Invoke for block rows, copy the result into . /// - public void CopyBlocksToColorBuffer() + public void CopyBlocksToColorBuffer(int step) { - var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component); - float maximumValue = MathF.Pow(2, this.ImagePostProcessor.RawJpeg.Precision) - 1; + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + var blockPp = new JpegBlockPostProcessor(this.RawJpeg, this.Component); + float maximumValue = MathF.Pow(2, this.RawJpeg.Precision) - 1; int destAreaStride = this.ColorBuffer.Width; + int yBlockStart = step * this.BlockRowsPerStep; + for (int y = 0; y < this.BlockRowsPerStep; y++) { - int yBlock = this.currentComponentRowInBlocks + y; + int yBlock = yBlockStart + y; - if (yBlock >= this.SizeInBlocks.Height) + if (yBlock >= spectralBuffer.Height) { break; } @@ -93,10 +88,10 @@ public void CopyBlocksToColorBuffer() int yBuffer = y * this.blockAreaSize.Height; Span colorBufferRow = this.ColorBuffer.GetRowSpan(yBuffer); - Span blockRow = this.Component.SpectralBlocks.GetRowSpan(yBlock); + Span blockRow = spectralBuffer.GetRowSpan(yBlock); // see: https://github.com/SixLabors/ImageSharp/issues/824 - int widthInBlocks = Math.Min(this.Component.SpectralBlocks.Width, this.SizeInBlocks.Width); + int widthInBlocks = Math.Min(spectralBuffer.Width, this.SizeInBlocks.Width); for (int xBlock = 0; xBlock < widthInBlocks; xBlock++) { @@ -107,7 +102,20 @@ public void CopyBlocksToColorBuffer() blockPp.ProcessBlockColorsInto(ref block, ref destAreaOrigin, destAreaStride, maximumValue); } } + } + + public void ClearSpectralBuffers() + { + Buffer2D spectralBlocks = this.Component.SpectralBlocks; + for (int i = 0; i < spectralBlocks.Height; i++) + { + spectralBlocks.GetRowSpan(i).Clear(); + } + } + public void CopyBlocksToColorBuffer() + { + this.CopyBlocksToColorBuffer(this.currentComponentRowInBlocks); this.currentComponentRowInBlocks += this.BlockRowsPerStep; } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index 827afe38da..3a136b4103 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -20,6 +20,14 @@ internal sealed class JpegFrame : IDisposable /// public bool Progressive { get; set; } + /// + /// Gets or sets a value indicating whether the frame is encoded using multiple scans (SOS markers). + /// + /// + /// This is true for progressive and baseline non-interleaved images. + /// + public bool MultiScan { get; set; } + /// /// Gets or sets the precision. /// @@ -28,12 +36,12 @@ internal sealed class JpegFrame : IDisposable /// /// Gets or sets the number of scanlines within the frame. /// - public int Scanlines { get; set; } + public int PixelHeight { get; set; } /// /// Gets or sets the number of samples per scanline. /// - public int SamplesPerLine { get; set; } + public int PixelWidth { get; set; } /// /// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4. @@ -95,8 +103,8 @@ public void Dispose() /// public void InitComponents() { - this.McusPerLine = (int)MathF.Ceiling(this.SamplesPerLine / 8F / this.MaxHorizontalFactor); - this.McusPerColumn = (int)MathF.Ceiling(this.Scanlines / 8F / this.MaxVerticalFactor); + this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)this.MaxHorizontalFactor * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)this.MaxVerticalFactor * 8); for (int i = 0; i < this.ComponentCount; i++) { @@ -104,5 +112,14 @@ public void InitComponents() component.Init(); } } + + public void AllocateComponents(bool fullScan) + { + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.AllocateSpectral(fullScan); + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs deleted file mode 100644 index 5b0331c85c..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Numerics; -using System.Threading; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using JpegColorConverter = SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters.JpegColorConverter; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - /// - /// Encapsulates the execution od post-processing algorithms to be applied on a to produce a valid :
- /// (1) Dequantization
- /// (2) IDCT
- /// (3) Color conversion form one of the -s into a buffer of RGBA values
- /// (4) Packing pixels from the buffer.
- /// These operations are executed in steps. - /// image rows are converted in one step, - /// which means that size of the allocated memory is limited (does not depend on ). - ///
- internal class JpegImagePostProcessor : IDisposable - { - private readonly Configuration configuration; - - /// - /// The number of block rows to be processed in one Step. - /// - public const int BlockRowsPerStep = 4; - - /// - /// The number of image pixel rows to be processed in one step. - /// - public const int PixelRowsPerStep = 4 * 8; - - /// - /// Temporal buffer to store a row of colors. - /// - private readonly IMemoryOwner rgbaBuffer; - - /// - /// The corresponding to the current determined by . - /// - private readonly JpegColorConverter colorConverter; - - /// - /// Initializes a new instance of the class. - /// - /// The to configure internal operations. - /// The representing the uncompressed spectral Jpeg data - public JpegImagePostProcessor(Configuration configuration, IRawJpegData rawJpeg) - { - this.configuration = configuration; - this.RawJpeg = rawJpeg; - IJpegComponent c0 = rawJpeg.Components[0]; - this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep; - this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - this.ComponentProcessors = new JpegComponentPostProcessor[rawJpeg.Components.Length]; - for (int i = 0; i < rawJpeg.Components.Length; i++) - { - this.ComponentProcessors[i] = new JpegComponentPostProcessor(memoryAllocator, this, rawJpeg.Components[i]); - } - - this.rgbaBuffer = memoryAllocator.Allocate(rawJpeg.ImageSizeInPixels.Width); - this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision); - } - - /// - /// Gets the instances. - /// - public JpegComponentPostProcessor[] ComponentProcessors { get; } - - /// - /// Gets the to be processed. - /// - public IRawJpegData RawJpeg { get; } - - /// - /// Gets the total number of post processor steps deduced from the height of the image and . - /// - public int NumberOfPostProcessorSteps { get; } - - /// - /// Gets the size of the temporary buffers we need to allocate into . - /// - public Size PostProcessorBufferSize { get; } - - /// - /// Gets the value of the counter that grows by each step by . - /// - public int PixelRowCounter { get; private set; } - - /// - public void Dispose() - { - foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) - { - cpp.Dispose(); - } - - this.rgbaBuffer.Dispose(); - } - - /// - /// Process all pixels into 'destination'. The image dimensions should match . - /// - /// The pixel type - /// The destination image - /// The token to request cancellation. - public void PostProcess(ImageFrame destination, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - this.PixelRowCounter = 0; - - if (this.RawJpeg.ImageSizeInPixels != destination.Size()) - { - throw new ArgumentException("Input image is not of the size of the processed one!"); - } - - while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) - { - cancellationToken.ThrowIfCancellationRequested(); - this.DoPostProcessorStep(destination); - } - } - - /// - /// Execute one step processing pixel rows into 'destination'. - /// - /// The pixel type - /// The destination image. - public void DoPostProcessorStep(ImageFrame destination) - where TPixel : unmanaged, IPixel - { - foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) - { - cpp.CopyBlocksToColorBuffer(); - } - - this.ConvertColorsInto(destination); - - this.PixelRowCounter += PixelRowsPerStep; - } - - /// - /// Convert and copy row of colors into 'destination' starting at row . - /// - /// The pixel type - /// The destination image - private void ConvertColorsInto(ImageFrame destination) - where TPixel : unmanaged, IPixel - { - int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep); - - var buffers = new Buffer2D[this.ComponentProcessors.Length]; - for (int i = 0; i < this.ComponentProcessors.Length; i++) - { - buffers[i] = this.ComponentProcessors[i].ColorBuffer; - } - - for (int yy = this.PixelRowCounter; yy < maxY; yy++) - { - int y = yy - this.PixelRowCounter; - - var values = new JpegColorConverter.ComponentValues(buffers, y); - this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan()); - - Span destRow = destination.GetPixelRowSpan(yy); - - // TODO: Investigate if slicing is actually necessary - PixelOperations.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs new file mode 100644 index 0000000000..e84d13ff16 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Converter used to convert jpeg spectral data. + /// + /// + /// This is tightly coupled with and . + /// + internal abstract class SpectralConverter + { + /// + /// Injects jpeg image decoding metadata. + /// + /// + /// This is guaranteed to be called only once at SOF marker by . + /// + /// instance containing decoder-specific parameters. + /// instance containing decoder-specific parameters. + public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + + /// + /// Called once per spectral stride for each component in . + /// This is called only for baseline interleaved jpegs. + /// + /// + /// Spectral 'stride' doesn't particularly mean 'single stride'. + /// Actual stride height depends on the subsampling factor of the given component. + /// + public abstract void ConvertStrideBaseline(); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs new file mode 100644 index 0000000000..9f3d4195cc --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Numerics; +using System.Threading; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal sealed class SpectralConverter : SpectralConverter, IDisposable + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + private CancellationToken cancellationToken; + + private JpegComponentPostProcessor[] componentProcessors; + + private JpegColorConverter colorConverter; + + private IMemoryOwner rgbaBuffer; + + private Buffer2D pixelBuffer; + + private int blockRowsPerStep; + + private int pixelRowsPerStep; + + private int pixelRowCounter; + + public SpectralConverter(Configuration configuration, CancellationToken cancellationToken) + { + this.configuration = configuration; + this.cancellationToken = cancellationToken; + } + + private bool Converted => this.pixelRowCounter >= this.pixelBuffer.Height; + + public Buffer2D PixelBuffer + { + get + { + if (!this.Converted) + { + int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); + + for (int step = 0; step < steps; step++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + this.ConvertNextStride(step); + } + } + + return this.pixelBuffer; + } + } + + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + MemoryAllocator allocator = this.configuration.MemoryAllocator; + + // iteration data + IJpegComponent c0 = frame.Components[0]; + + const int blockPixelHeight = 8; + this.blockRowsPerStep = c0.SamplingFactors.Height; + this.pixelRowsPerStep = this.blockRowsPerStep * blockPixelHeight; + + // pixel buffer for resulting image + this.pixelBuffer = allocator.Allocate2D(frame.PixelWidth, frame.PixelHeight, AllocationOptions.Clean); + + // component processors from spectral to Rgba32 + var postProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, this.pixelRowsPerStep); + this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length]; + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, jpegData, postProcessorBufferSize, frame.Components[i]); + } + + // single 'stride' rgba32 buffer for conversion between spectral and TPixel + this.rgbaBuffer = allocator.Allocate(frame.PixelWidth); + + // color converter from Rgba32 to TPixel + this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision); + } + + public override void ConvertStrideBaseline() + { + // Convert next pixel stride using single spectral `stride' + // Note that zero passing eliminates the need of virtual call from JpegComponentPostProcessor + this.ConvertNextStride(spectralStep: 0); + + // Clear spectral stride - this is VERY important as jpeg possibly won't fill entire buffer each stride + // Which leads to decoding artifacts + // Note that this code clears all buffers of the post processors, it's their responsibility to allocate only single stride + foreach (JpegComponentPostProcessor cpp in this.componentProcessors) + { + cpp.ClearSpectralBuffers(); + } + } + + public void Dispose() + { + if (this.componentProcessors != null) + { + foreach (JpegComponentPostProcessor cpp in this.componentProcessors) + { + cpp.Dispose(); + } + } + + this.rgbaBuffer?.Dispose(); + } + + private void ConvertNextStride(int spectralStep) + { + int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); + + var buffers = new Buffer2D[this.componentProcessors.Length]; + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep); + buffers[i] = this.componentProcessors[i].ColorBuffer; + } + + for (int yy = this.pixelRowCounter; yy < maxY; yy++) + { + int y = yy - this.pixelRowCounter; + + var values = new JpegColorConverter.ComponentValues(buffers, y); + this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan()); + + Span destRow = this.pixelBuffer.GetRowSpan(yy); + + // TODO: Investigate if slicing is actually necessary + PixelOperations.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow); + } + + this.pixelRowCounter += this.pixelRowsPerStep; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 9f3966de29..922e9797cb 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -97,6 +97,11 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// private AdobeMarker adobe; + /// + /// Scan decoder. + /// + private HuffmanScanDecoder scanDecoder; + /// /// Initializes a new instance of the class. /// @@ -213,18 +218,23 @@ public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStrea public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - this.ParseStream(stream, cancellationToken: cancellationToken); + using var spectralConverter = new SpectralConverter(this.Configuration, cancellationToken); + + var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken); + + this.ParseStream(stream, scanDecoder, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitDerivedMetadataProperties(); - return this.PostProcessIntoImage(cancellationToken); + + return new Image(this.Configuration, spectralConverter.PixelBuffer, this.Metadata); } /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ParseStream(stream, true, cancellationToken); + this.ParseStream(stream, scanDecoder: null, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -234,13 +244,17 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella } /// - /// Parses the input stream for file markers + /// Parses the input stream for file markers. /// - /// The input stream - /// Whether to decode metadata only. + /// The input stream. + /// Scan decoder used exclusively to decode SOS marker. /// The token to monitor cancellation. - public void ParseStream(BufferedReadStream stream, bool metadataOnly = false, CancellationToken cancellationToken = default) + internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDecoder, CancellationToken cancellationToken) { + bool metadataOnly = scanDecoder == null; + + this.scanDecoder = scanDecoder; + this.Metadata = new ImageMetadata(); // Check for the Start Of Image marker. @@ -852,19 +866,22 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, Precision = this.temp[0], - Scanlines = (this.temp[1] << 8) | this.temp[2], - SamplesPerLine = (this.temp[3] << 8) | this.temp[4], + PixelHeight = (this.temp[1] << 8) | this.temp[2], + PixelWidth = (this.temp[3] << 8) | this.temp[4], ComponentCount = this.temp[5] }; - if (this.Frame.SamplesPerLine == 0 || this.Frame.Scanlines == 0) + if (this.Frame.PixelWidth == 0 || this.Frame.PixelHeight == 0) { - JpegThrowHelper.ThrowInvalidImageDimensions(this.Frame.SamplesPerLine, this.Frame.Scanlines); + JpegThrowHelper.ThrowInvalidImageDimensions(this.Frame.PixelWidth, this.Frame.PixelHeight); } - this.ImageSizeInPixels = new Size(this.Frame.SamplesPerLine, this.Frame.Scanlines); + this.ImageSizeInPixels = new Size(this.Frame.PixelWidth, this.Frame.PixelHeight); this.ComponentCount = this.Frame.ComponentCount; + this.ColorSpace = this.DeduceJpegColorSpace(); + this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + if (!metadataOnly) { remaining -= length; @@ -881,7 +898,6 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, this.Frame.ComponentIds = new byte[this.ComponentCount]; this.Frame.ComponentOrder = new byte[this.ComponentCount]; this.Frame.Components = new JpegComponent[this.ComponentCount]; - this.ColorSpace = this.DeduceJpegColorSpace(); int maxH = 0; int maxV = 0; @@ -912,10 +928,12 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, this.Frame.MaxHorizontalFactor = maxH; this.Frame.MaxVerticalFactor = maxV; - this.ColorSpace = this.DeduceJpegColorSpace(); - this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; this.Frame.InitComponents(); + this.ImageSizeInMCU = new Size(this.Frame.McusPerLine, this.Frame.McusPerColumn); + + // This can be injected in SOF marker callback + this.scanDecoder.InjectFrameData(this.Frame, this); } } @@ -1016,6 +1034,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationTok } int selectorsCount = stream.ReadByte(); + this.Frame.MultiScan = this.Frame.ComponentCount != selectorsCount; for (int i = 0; i < selectorsCount; i++) { int componentIndex = -1; @@ -1049,20 +1068,26 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationTok int spectralEnd = this.temp[1]; int successiveApproximation = this.temp[2]; - var sd = new HuffmanScanDecoder( - stream, - this.Frame, - this.dcHuffmanTables, - this.acHuffmanTables, - selectorsCount, - this.resetInterval, - spectralStart, - spectralEnd, - successiveApproximation >> 4, - successiveApproximation & 15, - cancellationToken); - - sd.ParseEntropyCodedData(); + // All the comments below are for separate refactoring PR + // Main reason it's not fixed here is to make this commit less intrusive + + // Huffman tables can be calculated directly in the scan decoder class + this.scanDecoder.DcHuffmanTables = this.dcHuffmanTables; + this.scanDecoder.AcHuffmanTables = this.acHuffmanTables; + + // This can be injectd in DRI marker callback + this.scanDecoder.ResetInterval = this.resetInterval; + + // This can be passed as ParseEntropyCodedData() parameter as it is used only there + this.scanDecoder.ComponentsLength = selectorsCount; + + // This is okay to inject here, might be good to wrap it in a separate struct but not really necessary + this.scanDecoder.SpectralStart = spectralStart; + this.scanDecoder.SpectralEnd = spectralEnd; + this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4; + this.scanDecoder.SuccessiveLow = successiveApproximation & 15; + + this.scanDecoder.ParseEntropyCodedData(); } /// @@ -1087,32 +1112,5 @@ private ushort ReadUint16(BufferedReadStream stream) stream.Read(this.markerBuffer, 0, 2); return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); } - - /// - /// Post processes the pixels into the destination image. - /// - /// The pixel format. - /// The . - private Image PostProcessIntoImage(CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - if (this.ImageWidth == 0 || this.ImageHeight == 0) - { - JpegThrowHelper.ThrowInvalidImageDimensions(this.ImageWidth, this.ImageHeight); - } - - var image = Image.CreateUninitialized( - this.Configuration, - this.ImageWidth, - this.ImageHeight, - this.Metadata); - - using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this)) - { - postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken); - } - - return image; - } } } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index b43ff0422b..2aa9c53945 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -87,6 +87,21 @@ internal Image(Configuration configuration, int width, int height, ImageMetadata this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); } + /// + /// Initializes a new instance of the class + /// wrapping an external pixel bufferx. + /// + /// The configuration providing initialization code which allows extending the library. + /// Pixel buffer. + /// The images metadata. + internal Image( + Configuration configuration, + Buffer2D pixelBuffer, + ImageMetadata metadata) + : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata) + { + } + /// /// Initializes a new instance of the class /// wrapping an external . diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 68a102e3ce..9db666c374 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -4,6 +4,7 @@ using System.IO; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Tests; using SDSize = System.Drawing.Size; @@ -39,21 +40,46 @@ public void ParseStream() using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true }); - decoder.ParseStream(bufferedStream); + var scanDecoder = new HuffmanScanDecoder(bufferedStream, new NoopSpectralConverter(), cancellationToken: default); + decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); decoder.Dispose(); } - } - /* - | Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | - |---------------------------- |----------- |-------------- |--------------------- |---------:|----------:|----------:|------:|--------:|------:|------:|----------:| - | 'System.Drawing FULL' | Job-HITJFX | .NET 4.7.2 | Jpg/b(...)e.jpg [21] | 5.828 ms | 0.9885 ms | 0.0542 ms | 1.00 | 46.8750 | - | - | 211566 B | - | JpegDecoderCore.ParseStream | Job-HITJFX | .NET 4.7.2 | Jpg/b(...)e.jpg [21] | 5.833 ms | 0.2923 ms | 0.0160 ms | 1.00 | - | - | - | 12416 B | - | | | | | | | | | | | | | - | 'System.Drawing FULL' | Job-WPSKZD | .NET Core 2.1 | Jpg/b(...)e.jpg [21] | 6.018 ms | 2.1374 ms | 0.1172 ms | 1.00 | 46.8750 | - | - | 210768 B | - | JpegDecoderCore.ParseStream | Job-WPSKZD | .NET Core 2.1 | Jpg/b(...)e.jpg [21] | 4.382 ms | 0.9009 ms | 0.0494 ms | 0.73 | - | - | - | 12360 B | - | | | | | | | | | | | | | - | 'System.Drawing FULL' | Job-ZLSNRP | .NET Core 3.1 | Jpg/b(...)e.jpg [21] | 5.714 ms | 0.4078 ms | 0.0224 ms | 1.00 | - | - | - | 176 B | - | JpegDecoderCore.ParseStream | Job-ZLSNRP | .NET Core 3.1 | Jpg/b(...)e.jpg [21] | 4.239 ms | 1.0943 ms | 0.0600 ms | 0.74 | - | - | - | 12406 B | - */ + // We want to test only stream parsing and scan decoding, we don't need to convert spectral data to actual pixels + // Nor we need to allocate final pixel buffer + // Note: this still introduces virtual method call overhead for baseline interleaved images + // There's no way to eliminate it as spectral conversion is built into the scan decoding loop for memory footprint reduction + private class NoopSpectralConverter : SpectralConverter + { + public override void ConvertStrideBaseline() + { + } + + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + } + } + } } + +/* +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.1083 (20H2/October2020Update) +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.100-preview.3.21202.5 + [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT + Job-VAJCIU : .NET Core 2.1.26 (CoreCLR 4.6.29812.02, CoreFX 4.6.29812.01), X64 RyuJIT + Job-INPXCR : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT + Job-JRCLOJ : .NET Framework 4.8 (4.8.4390.0), X64 RyuJIT + +IterationCount=3 LaunchCount=1 WarmupCount=3 +| Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | +|---------------------------- |----------- |--------------------- |---------------------- |---------:|----------:|----------:|------:|--------:|------:|------:|----------:| +| 'System.Drawing FULL' | Job-VAJCIU | .NET Core 2.1 | Jpg/baseline/Lake.jpg | 5.196 ms | 0.7520 ms | 0.0412 ms | 1.00 | 46.8750 | - | - | 210,768 B | +| JpegDecoderCore.ParseStream | Job-VAJCIU | .NET Core 2.1 | Jpg/baseline/Lake.jpg | 3.467 ms | 0.0784 ms | 0.0043 ms | 0.67 | - | - | - | 12,416 B | +| | | | | | | | | | | | | +| 'System.Drawing FULL' | Job-INPXCR | .NET Core 3.1 | Jpg/baseline/Lake.jpg | 5.201 ms | 0.4105 ms | 0.0225 ms | 1.00 | - | - | - | 183 B | +| JpegDecoderCore.ParseStream | Job-INPXCR | .NET Core 3.1 | Jpg/baseline/Lake.jpg | 3.349 ms | 0.0468 ms | 0.0026 ms | 0.64 | - | - | - | 12,408 B | +| | | | | | | | | | | | | +| 'System.Drawing FULL' | Job-JRCLOJ | .NET Framework 4.7.2 | Jpg/baseline/Lake.jpg | 5.164 ms | 0.6524 ms | 0.0358 ms | 1.00 | 46.8750 | - | - | 211,571 B | +| JpegDecoderCore.ParseStream | Job-JRCLOJ | .NET Framework 4.7.2 | Jpg/baseline/Lake.jpg | 4.548 ms | 0.3357 ms | 0.0184 ms | 0.88 | - | - | - | 12,480 B | +*/ diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs index 9dd7e4c820..e6e82b9810 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Diagnostics; using SixLabors.ImageSharp.Tests.Formats.Jpg; using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; diff --git a/tests/ImageSharp.Tests/Common/NumericsTests.cs b/tests/ImageSharp.Tests/Common/NumericsTests.cs index 29eae6d488..62819af493 100644 --- a/tests/ImageSharp.Tests/Common/NumericsTests.cs +++ b/tests/ImageSharp.Tests/Common/NumericsTests.cs @@ -34,7 +34,7 @@ public void Log2_ZeroConvention() int expected = 0; int actual = Numerics.Log2(value); - Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}"); + Assert.Equal(expected, actual); } [Fact] @@ -47,7 +47,7 @@ public void Log2_PowersOfTwo() int expected = i; int actual = Numerics.Log2(value); - Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}"); + Assert.Equal(expected, actual); } } @@ -66,7 +66,35 @@ public void Log2_RandomValues(int seed, int count) int expected = Log2_ReferenceImplementation(value); int actual = Numerics.Log2(value); - Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}"); + Assert.Equal(expected, actual); + } + } + + private static uint DivideCeil_ReferenceImplementation(uint value, uint divisor) => (uint)MathF.Ceiling((float)value / divisor); + + [Fact] + public void DivideCeil_DivideZero() + { + uint expected = 0; + uint actual = Numerics.DivideCeil(0, 100); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 100)] + public void DivideCeil_RandomValues(int seed, int count) + { + var rng = new Random(seed); + for (int i = 0; i < count; i++) + { + uint value = (uint)rng.Next(); + uint divisor = (uint)rng.Next(); + + uint expected = DivideCeil_ReferenceImplementation(value, divisor); + uint actual = Numerics.DivideCeil(value, divisor); + + Assert.True(expected == actual, $"Expected: {expected}\nActual: {actual}\n{value} / {divisor} = {expected}"); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 2faea2611e..304dd93a63 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -17,8 +17,6 @@ public partial class JpegDecoderTests TestImages.Jpeg.Baseline.Jpeg400, TestImages.Jpeg.Baseline.Turtle420, TestImages.Jpeg.Baseline.Testorig420, - - // BUG: The following image has a high difference compared to the expected output: 1.0096% TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Issues.Fuzz.AccessViolationException922, TestImages.Jpeg.Baseline.Jpeg444, @@ -101,7 +99,7 @@ public partial class JpegDecoderTests [TestImages.Jpeg.Baseline.Bad.BadRST] = 0.0589f / 100, [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, - [TestImages.Jpeg.Baseline.Jpeg420Small] = 1.1f / 100, + [TestImages.Jpeg.Baseline.Jpeg420Small] = 0.287f / 100, [TestImages.Jpeg.Baseline.Turtle420] = 1.0f / 100, // Progressive: diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index d13a9696c3..a052ee88a5 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; - using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -79,7 +78,7 @@ public void ParseStream_BasicPropertiesAreCorrect() using var ms = new MemoryStream(bytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); - decoder.ParseStream(bufferedStream); + using Image image = decoder.Decode(bufferedStream, cancellationToken: default); // I don't know why these numbers are different. All I know is that the decoder works // and spectral data is exactly correct also. @@ -132,10 +131,10 @@ public async Task DecodeAsync_DegenerateMemoryRequest_ShouldTranslateTo_ImageFor [InlineData(0)] [InlineData(0.5)] [InlineData(0.9)] - public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel) + public async Task DecodeAsync_IsCancellable(int percentageOfStreamReadToCancel) { var cts = new CancellationTokenSource(); - var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); + string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); using var pausedStream = new PausedStream(file); pausedStream.OnWaiting(s => { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs deleted file mode 100644 index 93d9aee923..0000000000 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - -using Xunit; -using Xunit.Abstractions; - -namespace SixLabors.ImageSharp.Tests.Formats.Jpg -{ - [Trait("Format", "Jpg")] - public class JpegImagePostProcessorTests - { - public static string[] BaselineTestJpegs = - { - TestImages.Jpeg.Baseline.Calliphora, - TestImages.Jpeg.Baseline.Cmyk, - TestImages.Jpeg.Baseline.Ycck, - TestImages.Jpeg.Baseline.Jpeg400, - TestImages.Jpeg.Baseline.Testorig420, - TestImages.Jpeg.Baseline.Jpeg444, - }; - - public JpegImagePostProcessorTests(ITestOutputHelper output) - { - this.Output = output; - } - - private ITestOutputHelper Output { get; } - - private static void SaveBuffer(JpegComponentPostProcessor cp, TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = cp.ColorBuffer.ToGrayscaleImage(1f / 255f)) - { - image.DebugSave(provider, $"-C{cp.Component.Index}-"); - } - } - - [Theory] - [WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32)] - [WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)] - public void DoProcessorStep(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - string imageFile = provider.SourceFileOrDescription; - using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) - using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) - using (var imageFrame = new ImageFrame(Configuration.Default, decoder.ImageWidth, decoder.ImageHeight)) - { - pp.DoPostProcessorStep(imageFrame); - - JpegComponentPostProcessor[] cp = pp.ComponentProcessors; - - SaveBuffer(cp[0], provider); - SaveBuffer(cp[1], provider); - SaveBuffer(cp[2], provider); - } - } - - [Theory] - [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] - public void PostProcess(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - string imageFile = provider.SourceFileOrDescription; - using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) - using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) - using (var image = new Image(decoder.ImageWidth, decoder.ImageHeight)) - { - pp.PostProcess(image.Frames.RootFrame, default); - - image.DebugSave(provider); - - ImagingTestCaseUtility testUtil = provider.Utility; - testUtil.TestGroupName = nameof(JpegDecoderTests); - testUtil.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; - - using (Image referenceImage = - provider.GetReferenceOutputImage(appendPixelTypeToFileName: false)) - { - ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); - - this.Output.WriteLine($"*** {imageFile} ***"); - this.Output.WriteLine($"Difference: {report.DifferencePercentageString}"); - - // ReSharper disable once PossibleInvalidOperationException - Assert.True(report.TotalNormalizedDifference.Value < 0.005f); - } - } - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index de8103d639..a124ec1918 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -32,7 +32,7 @@ public void ColorSpace_IsDeducedCorrectly(string imageFile, object expectedColor { var expectedColorSpace = (JpegColorSpace)expectedColorSpaceValue; - using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) + using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile, metaDataOnly: true)) { Assert.Equal(expectedColorSpace, decoder.ColorSpace); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 91b1b9cd78..0b819bf13c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -4,9 +4,12 @@ using System; using System.IO; using System.Linq; - +using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; @@ -44,20 +47,25 @@ public SpectralJpegTests(ITestOutputHelper output) public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray(); [Theory(Skip = "Debug only, enable manually!")] + //[Theory] [WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)] public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider provider) where TPixel : unmanaged, IPixel { - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); - + // Calculating data from ImageSharp byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; + var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); using var ms = new MemoryStream(sourceBytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); - decoder.ParseStream(bufferedStream); - var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); - VerifyJpeg.SaveSpectralImage(provider, data); + // 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); + VerifyJpeg.SaveSpectralImage(provider, debugConverter.SpectralData); } [Theory] @@ -70,25 +78,31 @@ public void VerifySpectralCorrectness(TestImageProvider provider return; } - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + // Expected data from libjpeg + LibJpegTools.SpectralData libJpegData = LibJpegTools.ExtractSpectralData(provider.SourceFileOrDescription); + // Calculating data from ImageSharp byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; + var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); using var ms = new MemoryStream(sourceBytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); - decoder.ParseStream(bufferedStream); - var imageSharpData = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); - this.VerifySpectralCorrectnessImpl(provider, imageSharpData); + // 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); + + // Actual verification + this.VerifySpectralCorrectnessImpl(libJpegData, debugConverter.SpectralData); } - private void VerifySpectralCorrectnessImpl( - TestImageProvider provider, + private void VerifySpectralCorrectnessImpl( + LibJpegTools.SpectralData libJpegData, LibJpegTools.SpectralData imageSharpData) - where TPixel : unmanaged, IPixel { - LibJpegTools.SpectralData libJpegData = LibJpegTools.ExtractSpectralData(provider.SourceFileOrDescription); - bool equality = libJpegData.Equals(imageSharpData); this.Output.WriteLine("Spectral data equality: " + equality); @@ -108,11 +122,11 @@ private void VerifySpectralCorrectnessImpl( LibJpegTools.ComponentData libJpegComponent = libJpegData.Components[i]; LibJpegTools.ComponentData imageSharpComponent = imageSharpData.Components[i]; - (double total, double average) diff = LibJpegTools.CalculateDifference(libJpegComponent, imageSharpComponent); + (double total, double average) = LibJpegTools.CalculateDifference(libJpegComponent, imageSharpComponent); - this.Output.WriteLine($"Component{i}: {diff}"); - averageDifference += diff.average; - totalDifference += diff.total; + this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]"); + averageDifference += average; + totalDifference += total; tolerance += libJpegComponent.SpectralBlocks.DangerousGetSingleSpan().Length; } @@ -126,5 +140,71 @@ private void VerifySpectralCorrectnessImpl( Assert.True(totalDifference < tolerance); } + + private class DebugSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + private JpegFrame frame; + + private LibJpegTools.SpectralData spectralData; + + private int baselineScanRowCounter; + + public LibJpegTools.SpectralData SpectralData + { + get + { + // Due to underlying architecture, baseline interleaved jpegs would inject spectral data during parsing + // Progressive and multi-scan images must be loaded manually + if (this.frame.Progressive || this.frame.MultiScan) + { + LibJpegTools.ComponentData[] components = this.spectralData.Components; + for (int i = 0; i < components.Length; i++) + { + components[i].LoadSpectral(this.frame.Components[i]); + } + } + + return this.spectralData; + } + } + + public override void ConvertStrideBaseline() + { + // This would be called only for baseline non-interleaved images + // We must copy spectral strides here + LibJpegTools.ComponentData[] components = this.spectralData.Components; + for (int i = 0; i < components.Length; i++) + { + components[i].LoadSpectralStride(this.frame.Components[i].SpectralBlocks, this.baselineScanRowCounter); + } + + this.baselineScanRowCounter++; + + // As spectral buffers are reused for each stride decoding - we need to manually clear it like it's done in SpectralConverter + foreach (JpegComponent component in this.frame.Components) + { + Buffer2D spectralBlocks = component.SpectralBlocks; + for (int i = 0; i < spectralBlocks.Height; i++) + { + spectralBlocks.GetRowSpan(i).Clear(); + } + } + } + + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + + var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount]; + for (int i = 0; i < spectralComponents.Length; i++) + { + JpegComponent component = frame.Components[i]; + spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index); + } + + this.spectralData = new LibJpegTools.SpectralData(spectralComponents); + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs new file mode 100644 index 0000000000..353ae39f0f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Linq; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class SpectralToPixelConversionTests + { + public static readonly string[] BaselineTestJpegs = + { + TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk, TestImages.Jpeg.Baseline.Jpeg400, + TestImages.Jpeg.Baseline.Jpeg444, TestImages.Jpeg.Baseline.Testorig420, + TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Baseline.Bad.BadEOF, + TestImages.Jpeg.Baseline.MultiScanBaselineCMYK + }; + + public SpectralToPixelConversionTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + [Theory] + [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] + public void Decoder_PixelBufferComparison(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Stream + byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; + using var ms = new MemoryStream(sourceBytes); + using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); + + // Decoding + using var converter = new SpectralConverter(Configuration.Default, cancellationToken: default); + var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default); + decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); + + // Test metadata + provider.Utility.TestGroupName = nameof(JpegDecoderTests); + provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; + + // Comparison + using (Image image = new Image(Configuration.Default, converter.PixelBuffer, new ImageMetadata())) + using (Image referenceImage = provider.GetReferenceOutputImage(appendPixelTypeToFileName: false)) + { + ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); + + this.Output.WriteLine($"*** {provider.SourceFileOrDescription} ***"); + this.Output.WriteLine($"Difference: {report.DifferencePercentageString}"); + + // ReSharper disable once PossibleInvalidOperationException + Assert.True(report.TotalNormalizedDifference.Value < 0.005f); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index c6f4704f05..ccb7f6f1eb 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Abstractions; @@ -196,7 +197,14 @@ internal static JpegDecoderCore ParseJpegStream(string testFileName, bool metaDa using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); - decoder.ParseStream(bufferedStream, metaDataOnly); + if (metaDataOnly) + { + decoder.Identify(bufferedStream, cancellationToken: default); + } + else + { + using Image image = decoder.Decode(bufferedStream, cancellationToken: default); + } return decoder; } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs index 6f6032ee2e..edb8d457b7 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs @@ -56,23 +56,48 @@ internal void MakeBlock(short[] data, int y, int x) this.SpectralBlocks[x, y] = new Block8x8(data); } - public static ComponentData Load(JpegComponent c, int index) + public void LoadSpectralStride(Buffer2D data, int strideIndex) { - var result = new ComponentData( - c.WidthInBlocks, - c.HeightInBlocks, - index); + int startIndex = strideIndex * data.Height; + + int endIndex = Math.Min(this.HeightInBlocks, startIndex + data.Height); - for (int y = 0; y < result.HeightInBlocks; y++) + for (int y = startIndex; y < endIndex; y++) { - Span blockRow = c.SpectralBlocks.GetRowSpan(y); - for (int x = 0; x < result.WidthInBlocks; x++) + Span blockRow = data.GetRowSpan(y - startIndex); + for (int x = 0; x < this.WidthInBlocks; x++) { - short[] data = blockRow[x].ToArray(); - result.MakeBlock(data, y, x); + short[] block = blockRow[x].ToArray(); + + // x coordinate stays the same - we load entire stride + // y coordinate is tricky as we load single stride to full buffer - offset is needed + this.MakeBlock(block, y, x); } } + } + + public void LoadSpectral(JpegComponent c) + { + Buffer2D data = c.SpectralBlocks; + for (int y = 0; y < this.HeightInBlocks; y++) + { + Span blockRow = data.GetRowSpan(y); + for (int x = 0; x < this.WidthInBlocks; x++) + { + short[] block = blockRow[x].ToArray(); + this.MakeBlock(block, y, x); + } + } + } + + public static ComponentData Load(JpegComponent c, int index) + { + var result = new ComponentData( + c.WidthInBlocks, + c.HeightInBlocks, + index); + result.LoadSpectral(c); return result; } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs index 6ed7c15aed..2d0672f172 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.SpectralData.cs @@ -29,14 +29,6 @@ internal SpectralData(LibJpegTools.ComponentData[] components) this.Components = components; } - public static SpectralData LoadFromImageSharpDecoder(JpegDecoderCore decoder) - { - JpegComponent[] srcComponents = decoder.Frame.Components; - LibJpegTools.ComponentData[] destComponents = srcComponents.Select(LibJpegTools.ComponentData.Load).ToArray(); - - return new SpectralData(destComponents); - } - public Image TryCreateRGBSpectralImage() { if (this.ComponentCount != 3) diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png index c57b00d0e3..4032a32afb 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a76832570111a868ea6cb6e8287aae1976c575c94c63880c74346a4b5db5d305 -size 27007 +oid sha256:2b5e1d91fb6dc1ddb696fbee63331ba9c6ef3548b619c005887e60c5b01f4981 +size 27303