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