diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index 6e3866d154..ae97c7e54a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -247,8 +247,7 @@ public void ParseEntropyCodedData(int scanComponentCount) this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; - this.frame.AllocateComponents(fullScan); + this.frame.AllocateComponents(); if (this.frame.Progressive) { @@ -326,11 +325,13 @@ private void ParseBaselineData() if (this.scanComponentCount != 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataInterleaved(); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataSingleComponent(); this.spectralConverter.CommitConversion(); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs index 4879a50200..b333d32680 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. #if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters @@ -25,7 +26,9 @@ protected JpegColorConverterAvx(JpegColorSpace colorSpace, int precision) { } - public override bool IsAvailable => Avx.IsSupported; + public sealed override bool IsAvailable => Avx.IsSupported; + + public sealed override int ElementsPerBatch => Vector256.Count; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs index d5b0c679b6..8339206b4e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs @@ -35,6 +35,15 @@ protected JpegColorConverterBase(JpegColorSpace colorSpace, int precision) /// public abstract bool IsAvailable { get; } + /// + /// Gets a value indicating how many pixels are processed in a single batch. + /// + /// + /// This generally should be equal to register size, + /// e.g. 1 for scalar implementation, 8 for AVX implementation and so on. + /// + public abstract int ElementsPerBatch { get; } + /// /// Gets the of this converter. /// @@ -219,7 +228,7 @@ public ComponentValues(IReadOnlyList> componentBuffers, int row) /// /// List of component color processors. /// Row to convert - public ComponentValues(IReadOnlyList processors, int row) + public ComponentValues(IReadOnlyList processors, int row) { DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs index f3e5bdd5af..8cf8ad1d92 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs @@ -16,7 +16,9 @@ protected JpegColorConverterScalar(JpegColorSpace colorSpace, int precision) { } - public override bool IsAvailable => true; + public sealed override bool IsAvailable => true; + + public sealed override int ElementsPerBatch => 1; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs index 51b5a0db5a..6e0c0cff34 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs @@ -17,7 +17,7 @@ internal abstract partial class JpegColorConverterBase /// Even though real life data is guaranteed to be of size /// divisible by 8 newer SIMD instructions like AVX512 won't work with /// such data out of the box. These converters have fallback code - /// for 'remainder' data. + /// for remainder data. /// internal abstract class JpegColorConverterVector : JpegColorConverterBase { @@ -28,7 +28,9 @@ protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision) public sealed override bool IsAvailable => Vector.IsHardwareAccelerated && Vector.Count % 4 == 0; - public override void ConvertToRgbInplace(in ComponentValues values) + public sealed override int ElementsPerBatch => Vector.Count; + + public sealed override void ConvertToRgbInplace(in ComponentValues values) { DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs new file mode 100644 index 0000000000..87e85686ca --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Base class for processing component spectral data and converting it to raw color data. + /// + internal abstract class ComponentProcessor : IDisposable + { + public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize) + { + this.Frame = frame; + this.Component = component; + + this.BlockAreaSize = component.SubSamplingDivisors * blockSize; + this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( + postProcessorBufferSize.Width, + postProcessorBufferSize.Height, + this.BlockAreaSize.Height); + } + + protected JpegFrame Frame { get; } + + protected IJpegComponent Component { get; } + + protected Buffer2D ColorBuffer { get; } + + protected Size BlockAreaSize { get; } + + /// + /// Converts spectral data to color data accessible via . + /// + /// Spectral row index to convert. + public abstract void CopyBlocksToColorBuffer(int row); + + /// + /// Clears spectral buffers. + /// + /// + /// Should only be called during baseline interleaved decoding. + /// + public void ClearSpectralBuffers() + { + Buffer2D spectralBlocks = this.Component.SpectralBlocks; + for (int i = 0; i < spectralBlocks.Height; i++) + { + spectralBlocks.DangerousGetRowSpan(i).Clear(); + } + } + + /// + /// Gets converted color buffer row. + /// + /// Row index. + /// Color buffer row. + public Span GetColorBufferRowSpan(int row) => + this.ColorBuffer.DangerousGetRowSpan(row); + + public void Dispose() => this.ColorBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs new file mode 100644 index 0000000000..80cc689e3b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 1-to-1 scale. + /// + internal sealed class DirectComponentProcessor : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // Dequantize + workspaceBlock.MultiplyInPlace(ref this.dequantizationTable); + + // Convert from spectral to color + FloatingPointDCT.TransformIDCT(ref workspaceBlock); + + // To conform better to libjpeg we actually NEED TO loose precision here. + // This is because they store blocks as Int16 between all the operations. + // To be "more accurate", we need to emulate this by rounding! + workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); + + // Write to color buffer acording to sampling factors + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + workspaceBlock.ScaledCopyTo( + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs new file mode 100644 index 0000000000..801b2a3fbd --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 2-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor2 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 4) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // TODO: Optimize: implement all cases with scale-specific, loopless code! + CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); + + [MethodImpl(InliningOptions.ColdPath)] + static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + for (int y = 0; y < 4; y++) + { + int yy = y * verticalScale; + int y8 = y * 8; + + for (int x = 0; x < 4; x++) + { + int xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + int baseIdx = ((yy + i) * areaStride) + xx; + + for (int j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value; + } + } + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs new file mode 100644 index 0000000000..1c63abc932 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 4-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor4 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor4(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 2) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_2x2(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // TODO: Optimize: implement all cases with scale-specific, loopless code! + CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); + + [MethodImpl(InliningOptions.ColdPath)] + static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + for (int y = 0; y < 2; y++) + { + int yy = y * verticalScale; + int y8 = y * 8; + + for (int x = 0; x < 2; x++) + { + int xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + int baseIdx = ((yy + i) * areaStride) + xx; + + for (int j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value; + } + } + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs new file mode 100644 index 0000000000..03f0de7411 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 8-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor8 : ComponentProcessor + { + private readonly float dcDequantizatizer; + + public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 1) + => this.dcDequantizatizer = 0.125f * rawJpeg.QuantizationTables[component.QuantizationTableIndex][0]; + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + float dc = ScaledFloatingPointDCT.TransformIDCT_1x1(blockRow[xBlock][0], this.dcDequantizatizer, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + dc, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + destRef = value; + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + destRef = value; + Unsafe.Add(ref destRef, 1) = value; + Unsafe.Add(ref destRef, 0 + (nint)(uint)destStrideWidth) = value; + Unsafe.Add(ref destRef, 1 + (nint)(uint)destStrideWidth) = value; + return; + } + + // TODO: Optimize: implement all cases with scale-specific, loopless code! + for (int y = 0; y < verticalScale; y++) + { + for (int x = 0; x < horizontalScale; x++) + { + Unsafe.Add(ref destRef, (nint)(uint)x) = value; + } + + destRef = ref Unsafe.Add(ref destRef, (nint)(uint)destStrideWidth); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index aa0a2b1ba1..6f57dff99c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -109,10 +109,7 @@ public int ResetInterval // The successive approximation low bit end. public int SuccessiveLow { get; set; } - /// - /// Decodes the entropy coded data. - /// - /// Component count in the current scan. + /// public void ParseEntropyCodedData(int scanComponentCount) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -121,8 +118,7 @@ public void ParseEntropyCodedData(int scanComponentCount) this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; - this.frame.AllocateComponents(fullScan); + this.frame.AllocateComponents(); if (!this.frame.Progressive) { @@ -152,11 +148,13 @@ private void ParseBaselineData() { if (this.scanComponentCount != 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataInterleaved(); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataSingleComponent(); this.spectralConverter.CommitConversion(); } @@ -269,7 +267,7 @@ private void ParseBaselineDataNonInterleaved() private void ParseBaselineDataSingleComponent() { - var component = this.frame.Components[0] as JpegComponent; + JpegComponent component = this.frame.Components[0]; int mcuLines = this.frame.McusPerColumn; int w = component.WidthInBlocks; int h = component.SamplingFactors.Height; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs deleted file mode 100644 index 65b4c4262d..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - /// - /// Encapsulates spectral data to rgba32 processing for one component. - /// - internal class JpegComponentPostProcessor : IDisposable - { - /// - /// The size of the area in corresponding to one 8x8 Jpeg block - /// - private readonly Size blockAreaSize; - - /// - /// Jpeg frame instance containing required decoding metadata. - /// - private readonly JpegFrame frame; - - /// - /// Gets the maximal number of block rows being processed in one step. - /// - private readonly int blockRowsPerStep; - - /// - /// Gets the component containing decoding meta information. - /// - private readonly IJpegComponent component; - - /// - /// Gets the instance containing decoding meta information. - /// - private readonly IRawJpegData rawJpeg; - - /// - /// Initializes a new instance of the class. - /// - public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) - { - this.frame = frame; - - this.component = component; - this.rawJpeg = rawJpeg; - this.blockAreaSize = this.component.SubSamplingDivisors * 8; - this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( - postProcessorBufferSize.Width, - postProcessorBufferSize.Height, - this.blockAreaSize.Height); - - this.blockRowsPerStep = postProcessorBufferSize.Height / 8 / this.component.SubSamplingDivisors.Height; - } - - /// - /// Gets the temporary working buffer of color values. - /// - public Buffer2D ColorBuffer { get; } - - /// - public void Dispose() => this.ColorBuffer.Dispose(); - - /// - /// Convert raw spectral DCT data to color data and copy it to the color buffer . - /// - public void CopyBlocksToColorBuffer(int spectralStep) - { - Buffer2D spectralBuffer = this.component.SpectralBlocks; - - float maximumValue = this.frame.MaxColorChannelValue; - - int destAreaStride = this.ColorBuffer.Width; - - int yBlockStart = spectralStep * this.blockRowsPerStep; - - Size subSamplingDivisors = this.component.SubSamplingDivisors; - - Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex]; - Block8x8F workspaceBlock = default; - - for (int y = 0; y < this.blockRowsPerStep; y++) - { - int yBuffer = y * this.blockAreaSize.Height; - - Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); - Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); - - for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) - { - // Integer to float - workspaceBlock.LoadFrom(ref blockRow[xBlock]); - - // Dequantize - workspaceBlock.MultiplyInPlace(ref dequantTable); - - // Convert from spectral to color - FastFloatingPointDCT.TransformIDCT(ref workspaceBlock); - - // To conform better to libjpeg we actually NEED TO loose precision here. - // This is because they store blocks as Int16 between all the operations. - // To be "more accurate", we need to emulate this by rounding! - workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); - - // Write to color buffer according to sampling factors - int xColorBufferStart = xBlock * this.blockAreaSize.Width; - workspaceBlock.ScaledCopyTo( - ref colorBufferRow[xColorBufferStart], - destAreaStride, - subSamplingDivisors.Width, - subSamplingDivisors.Height); - } - } - } - - public void ClearSpectralBuffers() - { - Buffer2D spectralBlocks = this.component.SpectralBlocks; - for (int i = 0; i < spectralBlocks.Height; i++) - { - spectralBlocks.DangerousGetRowSpan(i).Clear(); - } - } - - public Span GetColorBufferRowSpan(int row) => - this.ColorBuffer.DangerousGetRowSpan(row); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index ad800a8caf..5f55a10635 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -139,8 +139,9 @@ public void Init(int maxSubFactorH, int maxSubFactorV) } } - public void AllocateComponents(bool fullScan) + public void AllocateComponents() { + bool fullScan = this.Progressive || this.MultiScan; for (int i = 0; i < this.ComponentCount; i++) { IJpegComponent component = this.Components[i]; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs index 96d0416ebf..bef23d89b3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -10,6 +10,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal abstract class SpectralConverter { + /// + /// Supported scaled spectral block sizes for scaled IDCT decoding. + /// + private static readonly int[] ScaledBlockSizes = new int[] + { + // 8 => 1, 1/8 of the original size + 1, + + // 8 => 2, 1/4 of the original size + 2, + + // 8 => 4, 1/2 of the original size + 4, + }; + /// /// Gets a value indicating whether this converter has converted spectral /// data of the current image or not. @@ -20,12 +35,19 @@ internal abstract class SpectralConverter /// Injects jpeg image decoding metadata. /// /// - /// This is guaranteed to be called only once at SOF marker by . + /// This should be called exactly once during SOF (Start Of Frame) marker. /// - /// instance containing decoder-specific parameters. - /// instance containing decoder-specific parameters. + /// Instance containing decoder-specific parameters. + /// Instance containing decoder-specific parameters. public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + /// + /// Initializes this spectral decoder instance for decoding. + /// This should be called exactly once after all markers which can alter + /// spectral decoding parameters. + /// + public abstract void PrepareForDecoding(); + /// /// Converts single spectral jpeg stride to color stride in baseline /// decoding mode. @@ -58,5 +80,48 @@ public void CommitConversion() /// The raw JPEG data. /// The color converter. protected virtual JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(jpegData.ColorSpace, frame.Precision); + + /// + /// Calculates image size with optional scaling. + /// + /// + /// Does not apply scalling if is null. + /// + /// Size of the image. + /// Target size of the image. + /// Spectral block size, equals to 8 if scaling is not applied. + /// Resulting image size, equals to if scaling is not applied. + public static Size CalculateResultingImageSize(Size size, Size? targetSize, out int blockPixelSize) + { + const int blockNativePixelSize = 8; + + blockPixelSize = blockNativePixelSize; + if (targetSize != null) + { + Size tSize = targetSize.Value; + + int fullBlocksWidth = (int)((uint)size.Width / blockNativePixelSize); + int fullBlocksHeight = (int)((uint)size.Height / blockNativePixelSize); + + // & (blockNativePixelSize - 1) is Numerics.Modulo8(), basically + int blockWidthRemainder = size.Width & (blockNativePixelSize - 1); + int blockHeightRemainder = size.Height & (blockNativePixelSize - 1); + + for (int i = 0; i < ScaledBlockSizes.Length; i++) + { + int blockSize = ScaledBlockSizes[i]; + int scaledWidth = (fullBlocksWidth * blockSize) + (int)Numerics.DivideCeil((uint)(blockWidthRemainder * blockSize), blockNativePixelSize); + int scaledHeight = (fullBlocksHeight * blockSize) + (int)Numerics.DivideCeil((uint)(blockHeightRemainder * blockSize), blockNativePixelSize); + + if (scaledWidth >= tSize.Width && scaledHeight >= tSize.Height) + { + blockPixelSize = blockSize; + return new Size(scaledWidth, scaledHeight); + } + } + } + + return size; + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 9eea220551..d460d9497c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -31,10 +31,14 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// private readonly Configuration configuration; + private JpegFrame frame; + + private IRawJpegData jpegData; + /// /// Jpeg component converters from decompressed spectral to color data. /// - private JpegComponentPostProcessor[] componentProcessors; + private ComponentProcessor[] componentProcessors; /// /// Color converter from jpeg color space to target pixel color space. @@ -66,13 +70,26 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// private int pixelRowCounter; + /// + /// Represent target size after decoding for scaling decoding mode. + /// + /// + /// Null if no scaling is required. + /// + private Size? targetSize; + /// /// Initializes a new instance of the class. /// /// The configuration. - public SpectralConverter(Configuration configuration) => + /// Optional target size for decoded image. + public SpectralConverter(Configuration configuration, Size? targetSize = null) + { this.configuration = configuration; + this.targetSize = targetSize; + } + /// /// Gets converted pixel buffer. /// @@ -86,6 +103,8 @@ public Buffer2D GetPixelBuffer(CancellationToken cancellationToken) { if (!this.Converted) { + this.PrepareForDecoding(); + int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); for (int step = 0; step < steps; step++) @@ -95,60 +114,11 @@ public Buffer2D GetPixelBuffer(CancellationToken cancellationToken) } } - var buffer = this.pixelBuffer; + Buffer2D buffer = this.pixelBuffer; this.pixelBuffer = null; return buffer; } - /// - public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) - { - MemoryAllocator allocator = this.configuration.MemoryAllocator; - - // iteration data - int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); - int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height); - - const int blockPixelHeight = 8; - this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight; - - // pixel buffer for resulting image - this.pixelBuffer = allocator.Allocate2D( - frame.PixelWidth, - frame.PixelHeight, - this.configuration.PreferContiguousImageBuffers); - this.paddedProxyPixelRow = allocator.Allocate(frame.PixelWidth + 3); - - // component processors from spectral to Rgba32 - const int blockPixelWidth = 8; - var postProcessorBufferSize = new Size(majorBlockWidth * blockPixelWidth, this.pixelRowsPerStep); - this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length]; - for (int i = 0; i < this.componentProcessors.Length; i++) - { - this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); - } - - // single 'stride' rgba32 buffer for conversion between spectral and TPixel - this.rgbBuffer = allocator.Allocate(frame.PixelWidth * 3); - - // color converter from Rgba32 to TPixel - this.colorConverter = this.GetColorConverter(frame, jpegData); - } - - /// - 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.ConvertStride(spectralStep: 0); - - foreach (JpegComponentPostProcessor cpp in this.componentProcessors) - { - cpp.ClearSpectralBuffers(); - } - } - /// /// Converts single spectral jpeg stride to color stride. /// @@ -199,12 +169,88 @@ private void ConvertStride(int spectralStep) this.pixelRowCounter += this.pixelRowsPerStep; } + /// + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.jpegData = jpegData; + } + + /// + public override void PrepareForDecoding() + { + DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once."); + + MemoryAllocator allocator = this.configuration.MemoryAllocator; + + // color converter from RGB to TPixel + JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData); + this.colorConverter = converter; + + // resulting image size + Size pixelSize = CalculateResultingImageSize(this.frame.PixelSize, this.targetSize, out int blockPixelSize); + + // iteration data + int majorBlockWidth = this.frame.Components.Max((component) => component.SizeInBlocks.Width); + int majorVerticalSamplingFactor = this.frame.Components.Max((component) => component.SamplingFactors.Height); + + this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize; + + // pixel buffer for resulting image + this.pixelBuffer = allocator.Allocate2D( + pixelSize.Width, + pixelSize.Height, + this.configuration.PreferContiguousImageBuffers); + this.paddedProxyPixelRow = allocator.Allocate(pixelSize.Width + 3); + + // component processors from spectral to RGB + int bufferWidth = majorBlockWidth * blockPixelSize; + int batchSize = converter.ElementsPerBatch; + int batchRemainder = bufferWidth & (batchSize - 1); + var postProcessorBufferSize = new Size(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep); + this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize); + + // single 'stride' rgba32 buffer for conversion between spectral and TPixel + this.rgbBuffer = allocator.Allocate(pixelSize.Width * 3); + } + + /// + public override void ConvertStrideBaseline() + { + // Convert next pixel stride using single spectral `stride' + // Note that zero passing eliminates extra virtual call + this.ConvertStride(spectralStep: 0); + + foreach (ComponentProcessor cpp in this.componentProcessors) + { + cpp.ClearSpectralBuffers(); + } + } + + protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize) + { + MemoryAllocator allocator = this.configuration.MemoryAllocator; + var componentProcessors = new ComponentProcessor[frame.Components.Length]; + for (int i = 0; i < componentProcessors.Length; i++) + { + componentProcessors[i] = blockPixelSize switch + { + 4 => new DownScalingComponentProcessor2(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 2 => new DownScalingComponentProcessor4(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 1 => new DownScalingComponentProcessor8(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + _ => new DirectComponentProcessor(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + }; + } + + return componentProcessors; + } + /// public void Dispose() { if (this.componentProcessors != null) { - foreach (JpegComponentPostProcessor cpp in this.componentProcessors) + foreach (ComponentProcessor cpp in this.componentProcessors) { cpp.Dispose(); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 85b72ab6ee..24ab9f8d1f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -139,8 +139,8 @@ private bool IsStreamFlushNeeded public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); - FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -202,8 +202,8 @@ public void Encode444(Image pixels, ref Block8x8F luminanceQuant public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); - FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -271,7 +271,7 @@ public void Encode420(Image pixels, ref Block8x8F luminanceQuant public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -319,7 +319,7 @@ public void EncodeGrayscale(Image pixels, ref Block8x8F luminanc public void EncodeRgb(Image pixels, ref Block8x8F quantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref quantTable); + FloatingPointDCT.AdjustToFDCT(ref quantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -391,7 +391,7 @@ public void EncodeRgb(Image pixels, ref Block8x8F quantTable, Ca block.AddInPlace(-128f); // Discrete cosine transform - FastFloatingPointDCT.TransformFDCT(ref block); + FloatingPointDCT.TransformFDCT(ref block); // Quantization Block8x8F.Quantize(ref block, ref spectralBlock, ref quant); diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs similarity index 99% rename from src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs rename to src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs index 22dee95b44..19349e454b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal static partial class FastFloatingPointDCT + internal static partial class FloatingPointDCT { /// /// Apply floating point FDCT inplace using simd operations. diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs similarity index 93% rename from src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs rename to src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs index 34277f1ae1..37ec6e1905 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs @@ -12,9 +12,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { /// - /// Contains inaccurate, but fast forward and inverse DCT implementations. + /// Contains floating point forward and inverse DCT implementations /// - internal static partial class FastFloatingPointDCT + /// + /// Based on "Arai, Agui and Nakajima" algorithm. + /// + internal static partial class FloatingPointDCT { #pragma warning disable SA1310, SA1311, IDE1006 // naming rules violation warnings private static readonly Vector4 mm128_F_0_7071 = new(0.707106781f); @@ -70,8 +73,8 @@ public static void AdjustToIDCT(ref Block8x8F quantTable) ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); for (nint i = 0; i < Block8x8F.Size; i++) { - tableRef = 0.125f * tableRef * Unsafe.Add(ref multipliersRef, i); - tableRef = ref Unsafe.Add(ref tableRef, 1); + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef * Unsafe.Add(ref multipliersRef, i); } // Spectral macroblocks are transposed before quantization @@ -89,8 +92,8 @@ public static void AdjustToFDCT(ref Block8x8F quantTable) ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); for (nint i = 0; i < Block8x8F.Size; i++) { - tableRef = 0.125f / (tableRef * Unsafe.Add(ref multipliersRef, i)); - tableRef = ref Unsafe.Add(ref tableRef, 1); + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f / (elemRef * Unsafe.Add(ref multipliersRef, i)); } // Spectral macroblocks are not transposed before quantization @@ -103,7 +106,7 @@ public static void AdjustToFDCT(ref Block8x8F quantTable) /// Apply 2D floating point IDCT inplace. /// /// - /// Input block must be dequantized before this method with table + /// Input block must be dequantized with quantization table /// adjusted by . /// /// Input block. @@ -125,8 +128,8 @@ public static void TransformIDCT(ref Block8x8F block) /// Apply 2D floating point IDCT inplace. /// /// - /// Input block must be quantized after this method with table adjusted - /// by . + /// Input block must be quantized after this method with quantization + /// table adjusted by . /// /// Input block. public static void TransformFDCT(ref Block8x8F block) @@ -221,7 +224,7 @@ static void IDCT8x4_Vector4(ref Vector4 vecRef) /// Apply floating point FDCT inplace using API. /// /// Input block. - public static void FDCT_Vector4(ref Block8x8F block) + private static void FDCT_Vector4(ref Block8x8F block) { // First pass - process columns FDCT8x4_Vector4(ref block.V0L); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs new file mode 100644 index 0000000000..037995ea13 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0078 +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Contains floating point forward DCT implementations with built-in scaling. + /// + /// + /// Based on "Loeffler, Ligtenberg, and Moschytz" algorithm. + /// + internal static class ScaledFloatingPointDCT + { +#pragma warning disable SA1310 + private const float FP32_0_541196100 = 0.541196100f; + private const float FP32_0_765366865 = 0.765366865f; + private const float FP32_1_847759065 = 1.847759065f; + private const float FP32_0_211164243 = 0.211164243f; + private const float FP32_1_451774981 = 1.451774981f; + private const float FP32_2_172734803 = 2.172734803f; + private const float FP32_1_061594337 = 1.061594337f; + private const float FP32_0_509795579 = 0.509795579f; + private const float FP32_0_601344887 = 0.601344887f; + private const float FP32_0_899976223 = 0.899976223f; + private const float FP32_2_562915447 = 2.562915447f; + private const float FP32_0_720959822 = 0.720959822f; + private const float FP32_0_850430095 = 0.850430095f; + private const float FP32_1_272758580 = 1.272758580f; + private const float FP32_3_624509785 = 3.624509785f; +#pragma warning restore SA1310 + + /// + /// Adjusts given quantization table for usage with IDCT algorithms + /// from . + /// + /// Quantization table to adjust. + public static void AdjustToIDCT(ref Block8x8F quantTable) + { + ref float tableRef = ref Unsafe.As(ref quantTable); + for (nint i = 0; i < Block8x8F.Size; i++) + { + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef; + } + + // Spectral macroblocks are transposed before quantization + // so we must transpose quantization table + quantTable.TransposeInplace(); + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 4x4 result. + /// + /// + /// Resulting matrix is stored in the top left 4x4 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_4x4(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process row 4, second pass doesn't use it + if (ctr == 4) + { + continue; + } + + // Even part + float tmp0 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0] * 2; + + float z2 = block[(ctr * 8) + 2] * dequantTable[(ctr * 8) + 2]; + float z3 = block[(ctr * 8) + 6] * dequantTable[(ctr * 8) + 6]; + + float tmp2 = (z2 * FP32_1_847759065) + (z3 * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + z2 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + z3 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + float z4 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // temporal result is saved to +4 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 0 + 4] = (tmp10 + tmp2) / 2; + block[(ctr * 8) + 3 + 4] = (tmp10 - tmp2) / 2; + block[(ctr * 8) + 1 + 4] = (tmp12 + tmp0) / 2; + block[(ctr * 8) + 2 + 4] = (tmp12 - tmp0) / 2; + } + + for (int ctr = 0; ctr < 4; ctr++) + { + // Even part + float tmp0 = block[ctr + (8 * 0) + 4] * 2; + + float tmp2 = (block[ctr + (8 * 2) + 4] * FP32_1_847759065) + (block[ctr + (8 * 6) + 4] * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[ctr + (8 * 7) + 4]; + float z2 = block[ctr + (8 * 5) + 4]; + float z3 = block[ctr + (8 * 3) + 4]; + float z4 = block[ctr + (8 * 1) + 4]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // Save results to the top left 4x4 subregion + block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp2) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 3] = MathF.Round(Numerics.Clamp(((tmp10 - tmp2) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp12 + tmp0) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 2] = MathF.Round(Numerics.Clamp(((tmp12 - tmp0) / 2) + normalizationValue, 0, maxValue)); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 2x2 result. + /// + /// + /// Resulting matrix is stored in the top left 2x2 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_2x2(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process rows 2/4/6, second pass doesn't use it + if (ctr == 2 || ctr == 4 || ctr == 6) + { + continue; + } + + // Even part + float tmp0; + float z1 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0]; + float tmp10 = z1 * 4; + + // Odd part + z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + tmp0 = z1 * -FP32_0_720959822; + z1 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + tmp0 += z1 * FP32_0_850430095; + z1 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + tmp0 += z1 * -FP32_1_272758580; + z1 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + tmp0 += z1 * FP32_3_624509785; + + // temporal result is saved to +2 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 2] = (tmp10 + tmp0) / 4; + block[(ctr * 8) + 3] = (tmp10 - tmp0) / 4; + } + + for (int ctr = 0; ctr < 2; ctr++) + { + // Even part + float tmp10 = block[ctr + (8 * 0) + 2] * 4; + + // Odd part + float tmp0 = (block[ctr + (8 * 7) + 2] * -FP32_0_720959822) + + (block[ctr + (8 * 5) + 2] * FP32_0_850430095) + + (block[ctr + (8 * 3) + 2] * -FP32_1_272758580) + + (block[ctr + (8 * 1) + 2] * FP32_3_624509785); + + // Save results to the top left 2x2 subregion + block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp0) / 4) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp10 - tmp0) / 4) + normalizationValue, 0, maxValue)); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 1x1 result. + /// + /// Direct current term value from input block. + /// Dequantization value. + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue) + => MathF.Round(Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue)); + } +} +#pragma warning restore IDE0078 diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 9df1a7b4f2..7f23d1ac85 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -3,6 +3,8 @@ using System.IO; using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -29,6 +31,42 @@ public Image Decode(Configuration configuration, Stream stream, public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); + /// + /// Placeholder summary. + /// + /// Placeholder2 + /// Placeholder3 + /// Placeholder4 + /// Placeholder5 + /// Placeholder6 + internal Image DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) + => this.DecodeInto(configuration, stream, targetSize, cancellationToken); + + /// + /// Decodes and downscales the image from the specified stream if possible. + /// + /// The pixel format. + /// Configuration. + /// Stream. + /// Target size. + /// Cancellation token. + internal Image DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + using var decoder = new JpegDecoderCore(configuration, this); + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + try + { + return decoder.DecodeInto(bufferedReadStream, targetSize, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(((IImageDecoderInternals)decoder).Dimensions, ex); + } + } + /// public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 6b8d3b0d22..d5997e4128 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -201,34 +201,48 @@ public static JpegFileMarker FindNextFileMarker(BufferedReadStream stream) /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - using var spectralConverter = new SpectralConverter(this.Configuration); + => this.Decode(stream, targetSize: null, cancellationToken); - this.ParseStream(stream, spectralConverter, cancellationToken); + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ParseStream(stream, spectralConverter: null, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitXmpProfile(); this.InitDerivedMetadataProperties(); - return new Image( - this.Configuration, - spectralConverter.GetPixelBuffer(cancellationToken), - this.Metadata); + Size pixelSize = this.Frame.PixelSize; + return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); } - /// - public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + /// + /// Decodes and downscales the image from the specified stream if possible. + /// + /// The pixel format. + /// Stream. + /// Target size. + /// Cancellation token. + internal Image DecodeInto(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.Decode(stream, targetSize, cancellationToken); + + private Image Decode(BufferedReadStream stream, Size? targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { - this.ParseStream(stream, spectralConverter: null, cancellationToken); + using var spectralConverter = new SpectralConverter(this.Configuration, targetSize); + this.ParseStream(stream, spectralConverter, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitXmpProfile(); this.InitDerivedMetadataProperties(); - Size pixelSize = this.Frame.PixelSize; - return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); + return new Image( + this.Configuration, + spectralConverter.GetPixelBuffer(cancellationToken), + this.Metadata); } /// @@ -1169,9 +1183,6 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in break; } } - - // Adjusting table for IDCT step during decompression - FastFloatingPointDCT.AdjustToIDCT(ref table); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs index 6895c391b4..79daa7df48 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs @@ -79,4 +79,21 @@ public void Cleanup() | 'Baseline 4:2:0 Interleaved' | 8.458 ms | 0.0289 ms | 0.0256 ms | | 'Baseline 4:0:0 (grayscale)' | 1.550 ms | 0.0050 ms | 0.0044 ms | | 'Progressive 4:2:0 Non-Interleaved' | 13.220 ms | 0.0449 ms | 0.0398 ms | + + +FRESH BENCHMARKS FOR NEW SPECTRAL CONVERSION SETUP + +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +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.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + + +| Method | Mean | Error | StdDev | +|------------------------------------ |----------:|----------:|----------:| +| 'Baseline 4:4:4 Interleaved' | 10.734 ms | 0.0287 ms | 0.0254 ms | +| 'Baseline 4:2:0 Interleaved' | 8.517 ms | 0.0401 ms | 0.0356 ms | +| 'Baseline 4:0:0 (grayscale)' | 1.442 ms | 0.0051 ms | 0.0045 ms | +| 'Progressive 4:2:0 Non-Interleaved' | 12.740 ms | 0.0832 ms | 0.0730 ms | */ diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 90cbbb53c7..34aa111448 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -57,6 +57,10 @@ public override void ConvertStrideBaseline() public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { } + + public override void PrepareForDecoding() + { + } } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs deleted file mode 100644 index 57dffffe30..0000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortMultiFramework))] - public class LoadResizeSave_Aggregate : MultiImageBenchmarkBase - { - protected override IEnumerable InputImageSubfoldersOrFiles => - new[] - { - TestImages.Jpeg.BenchmarkSuite.Jpeg400_SmallMonochrome, - TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr, - TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, - TestImages.Jpeg.BenchmarkSuite.MissingFF00ProgressiveBedroom159_MidSize420YCbCr, - TestImages.Jpeg.BenchmarkSuite.ExifGetString750Transform_Huge420YCbCr, - }; - - [Params(InputImageCategory.AllImages)] - public override InputImageCategory InputCategory { get; set; } - - private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); - - private byte[] destBytes; - - public override void Setup() - { - base.Setup(); - - this.configuration.MaxDegreeOfParallelism = 1; - const int MaxOutputSizeInBytes = 2 * 1024 * 1024; // ~2 MB - this.destBytes = new byte[MaxOutputSizeInBytes]; - } - - [Benchmark(Baseline = true)] - public void SystemDrawing() - => this.ForEachStream( - sourceStream => - { - using (var destStream = new MemoryStream(this.destBytes)) - using (var source = System.Drawing.Image.FromStream(sourceStream)) - using (var destination = new Bitmap(source.Width / 4, source.Height / 4)) - { - using (var g = Graphics.FromImage(destination)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - g.DrawImage(source, 0, 0, 400, 400); - } - - destination.Save(destStream, ImageFormat.Jpeg); - } - - return null; - }); - - [Benchmark] - public void ImageSharp() - => this.ForEachStream( - sourceStream => - { - using (var source = Image.Load( - this.configuration, - sourceStream, - new JpegDecoder { IgnoreMetadata = true })) - { - using var destStream = new MemoryStream(this.destBytes); - source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); - source.SaveAsJpeg(destStream); - } - - return null; - }); - } -} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs deleted file mode 100644 index b0621ca18e..0000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests; -using SDImage = System.Drawing.Image; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortMultiFramework))] - public class LoadResizeSave_ImageSpecific - { - private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); - - private byte[] sourceBytes; - - private byte[] destBytes; - - private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); - - [Params( - TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, - TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr, - TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr)] - - public string TestImage { get; set; } - - [Params(false, true)] - public bool ParallelExec { get; set; } - - [GlobalSetup] - public void Setup() - { - this.configuration.MaxDegreeOfParallelism = - this.ParallelExec ? Environment.ProcessorCount : 1; - - this.sourceBytes = File.ReadAllBytes(this.TestImageFullPath); - - this.destBytes = new byte[this.sourceBytes.Length * 2]; - } - - [Benchmark(Baseline = true)] - public void SystemDrawing() - { - using var sourceStream = new MemoryStream(this.sourceBytes); - using var destStream = new MemoryStream(this.destBytes); - using var source = SDImage.FromStream(sourceStream); - using var destination = new Bitmap(source.Width / 4, source.Height / 4); - using (var g = Graphics.FromImage(destination)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - g.DrawImage(source, 0, 0, 400, 400); - } - - destination.Save(destStream, ImageFormat.Jpeg); - } - - [Benchmark] - public void ImageSharp() - { - using (var source = Image.Load(this.configuration, this.sourceBytes, new JpegDecoder { IgnoreMetadata = true })) - using (var destStream = new MemoryStream(this.destBytes)) - { - source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); - source.SaveAsJpeg(destStream); - } - } - - // RESULTS (2018 October 31): - // - // BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134 - // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores - // Frequency=2742191 Hz, Resolution=364.6719 ns, Timer=TSC - // .NET Core SDK=2.1.403 - // [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT - // Job-ZPEZGV : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 - // Job-SGOCJT : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT - // - // Method | Runtime | TestImage | ParallelExec | Mean | Error | StdDev | Scaled | ScaledSD | Gen 0 | Allocated | - // -------------- |-------- |----------------------------- |------------- |----------:|----------:|----------:|-------:|---------:|---------:|----------:| - // SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | False | 64.88 ms | 3.735 ms | 0.2110 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB | - // ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | False | 129.53 ms | 23.423 ms | 1.3234 ms | 2.00 | 0.02 | - | 50.09 KB | - // | | | | | | | | | | | - // SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | False | 65.87 ms | 10.488 ms | 0.5926 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB | - // ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | False | 92.00 ms | 7.241 ms | 0.4091 ms | 1.40 | 0.01 | - | 46.36 KB | - // | | | | | | | | | | | - // SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | True | 64.23 ms | 5.998 ms | 0.3389 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB | - // ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | True | 82.63 ms | 29.320 ms | 1.6566 ms | 1.29 | 0.02 | - | 57.59 KB | - // | | | | | | | | | | | - // SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | True | 64.20 ms | 6.560 ms | 0.3707 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB | - // ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | True | 68.08 ms | 18.376 ms | 1.0383 ms | 1.06 | 0.01 | - | 50.49 KB | - } -} diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs index 47f9b36c12..dd9d05cd46 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs @@ -3,7 +3,6 @@ using System; using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave { @@ -37,10 +36,10 @@ private void ForEachImage(Action action, int maxDegreeOfParallelism) public int[] ParallelismValues { get; } = { - Environment.ProcessorCount, + // Environment.ProcessorCount, // Environment.ProcessorCount / 2, // Environment.ProcessorCount / 4, - // 1 + 1 }; [Benchmark] @@ -49,10 +48,7 @@ private void ForEachImage(Action action, int maxDegreeOfParallelism) [Benchmark(Baseline = true)] [ArgumentsSource(nameof(ParallelismValues))] - public void ImageSharp(int maxDegreeOfParallelism) - { - this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); - } + public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); [Benchmark] [ArgumentsSource(nameof(ParallelismValues))] @@ -75,3 +71,24 @@ public void ImageSharp(int maxDegreeOfParallelism) public void NetVips(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.NetVipsResize, maxDegreeOfParallelism); } } + +/* +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.300 + [Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT + ShortRun : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 + +| Method | maxDegreeOfParallelism | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------- |----------------------- |-----------:|------------:|----------:|------:|--------:|------:|------:|------:|----------:| +| SystemDrawing | 1 | 3,624.2 ms | 721.39 ms | 39.54 ms | 3.30 | 0.04 | - | - | - | 12 KB | +| ImageSharp | 1 | 1,098.4 ms | 45.64 ms | 2.50 ms | 1.00 | 0.00 | - | - | - | 717 KB | +| Magick | 1 | 4,089.8 ms | 905.06 ms | 49.61 ms | 3.72 | 0.04 | - | - | - | 43 KB | +| MagicScaler | 1 | 888.0 ms | 168.33 ms | 9.23 ms | 0.81 | 0.01 | - | - | - | 105 KB | +| SkiaBitmap | 1 | 2,934.4 ms | 2,023.43 ms | 110.91 ms | 2.67 | 0.10 | - | - | - | 43 KB | +| SkiaBitmapDecodeToTargetSize | 1 | 892.3 ms | 115.54 ms | 6.33 ms | 0.81 | 0.01 | - | - | - | 48 KB | +| NetVips | 1 | 806.8 ms | 86.23 ms | 4.73 ms | 0.73 | 0.01 | - | - | - | 42 KB | +*/ diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 37d5f4edec..c26de91590 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -204,23 +204,30 @@ public void SystemDrawingResize(string input) public void ImageSharpResize(string input) { - using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); + using FileStream inputStream = File.Open(input, FileMode.Open); + using FileStream outputStream = File.Open(this.OutputPath(input), FileMode.Create); // Resize it to fit a 150x150 square - using var image = ImageSharpImage.Load(input); + var targetSize = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize); + var decoder = new JpegDecoder(); + using ImageSharpImage image = decoder.DecodeInto(Configuration.Default, inputStream, targetSize, default); this.LogImageProcessed(image.Width, image.Height); image.Mutate(i => i.Resize(new ResizeOptions { - Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize), - Mode = ResizeMode.Max + Size = targetSize, + Mode = ResizeMode.Max, + Sampler = KnownResamplers.Box })); // Reduce the size of the file image.Metadata.ExifProfile = null; + image.Metadata.XmpProfile = null; + image.Metadata.IccProfile = null; + image.Metadata.IptcProfile = null; // Save the results - image.Save(output, this.imageSharpJpegEncoder); + image.Save(outputStream, this.imageSharpJpegEncoder); } public async Task ImageSharpResizeAsync(string input) @@ -231,6 +238,7 @@ public async Task ImageSharpResizeAsync(string input) using ImageSharpImage image = await ImageSharpImage.LoadAsync(input); this.LogImageProcessed(image.Width, image.Height); + // Resize checks whether image size and target sizes are equal image.Mutate(i => i.Resize(new ResizeOptions { Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize), diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 0bf1189735..fc5e8f6bd3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; @@ -14,8 +15,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public static class DCTTests { - private const int MaxAllowedValue = short.MaxValue; - private const int MinAllowedValue = short.MinValue; + // size of input values is 10 bit max + private const float MaxInputValue = 1023; + private const float MinInputValue = -1024; + + // output value range is 12 bit max + private const float MaxOutputValue = 4096; + private const float NormalizationValue = MaxOutputValue / 2; internal static Block8x8F CreateBlockFromScalar(float value) { @@ -41,7 +47,7 @@ public FastFloatingPoint(ITestOutputHelper output) [InlineData(3)] public void LLM_TransformIDCT_CompareToNonOptimized(int seed) { - float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = Block8x8F.Load(sourceArray); @@ -56,14 +62,14 @@ public void LLM_TransformIDCT_CompareToNonOptimized(int seed) Block8x8F dequantMatrix = CreateBlockFromScalar(1); // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); srcBlock.MultiplyInPlace(ref dequantMatrix); // IDCT calculation - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); this.CompareBlocks(expected, srcBlock, 1f); } @@ -74,7 +80,7 @@ public void LLM_TransformIDCT_CompareToNonOptimized(int seed) [InlineData(3)] public void LLM_TransformIDCT_CompareToAccurate(int seed) { - float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = Block8x8F.Load(sourceArray); @@ -89,21 +95,18 @@ public void LLM_TransformIDCT_CompareToAccurate(int seed) Block8x8F dequantMatrix = CreateBlockFromScalar(1); // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); srcBlock.MultiplyInPlace(ref dequantMatrix); // IDCT calculation - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); this.CompareBlocks(expected, srcBlock, 1f); } - // Inverse transform - // This test covers entire IDCT conversion chain - // This test checks all hardware implementations [Theory] [InlineData(1)] [InlineData(2)] @@ -113,7 +116,7 @@ static void RunTest(string serialized) { int seed = FeatureTestRunner.Deserialize(serialized); - Span src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = default(Block8x8F); srcBlock.LoadFrom(src); @@ -132,13 +135,13 @@ static void RunTest(string serialized) // Dequantization using unit matrix - no values are upscaled // as quant matrix is all 1's // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); srcBlock.MultiplyInPlace(ref dequantMatrix); // testee // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); float[] actualDest = srcBlock.ToArray(); @@ -156,9 +159,170 @@ static void RunTest(string serialized) HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } - // Forward transform - // This test covers entire FDCT conversion chain - // This test checks all hardware implementations + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_4x4(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 4, 4); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + srcBlock.TransposeInplace(); + ScaledFloatingPointDCT.TransformIDCT_4x4(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue); + + Span expectedSpan = expectedDest.AsSpan(); + Span actualSpan = srcBlock.ToArray().AsSpan(); + + // resulting matrix is 4x4 + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + AssertScaledElementEquality(expectedSpan.Slice((y * 16) + (x * 2)), actualSpan.Slice((y * 8) + x)); + } + } + + static void AssertScaledElementEquality(Span expected, Span actual) + { + float average2x2 = 0f; + for (int y = 0; y < 2; y++) + { + int y8 = y * 8; + for (int x = 0; x < 2; x++) + { + float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue); + average2x2 += clamped; + } + } + + average2x2 = MathF.Round(average2x2 / 4f); + + Assert.Equal((int)average2x2, (int)actual[0]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_2x2(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 2, 2); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + srcBlock.TransposeInplace(); + ScaledFloatingPointDCT.TransformIDCT_2x2(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue); + + Span expectedSpan = expectedDest.AsSpan(); + Span actualSpan = srcBlock.ToArray().AsSpan(); + + // resulting matrix is 2x2 + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + AssertScaledElementEquality(expectedSpan.Slice((y * 32) + (x * 4)), actualSpan.Slice((y * 8) + x)); + } + } + + static void AssertScaledElementEquality(Span expected, Span actual) + { + float average4x4 = 0f; + for (int y = 0; y < 4; y++) + { + int y8 = y * 8; + for (int x = 0; x < 4; x++) + { + float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue); + average4x4 += clamped; + } + } + + average4x4 = MathF.Round(average4x4 / 16f); + + Assert.Equal((int)average4x4, (int)actual[0]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_1x1(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 1, 1); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + // But DC lays on main diagonal which is not changed by transposition + float actual = ScaledFloatingPointDCT.TransformIDCT_1x1( + srcBlock[0], + dequantMatrix[0], + NormalizationValue, + MaxOutputValue); + + float expected = MathF.Round(Numerics.Clamp(expectedDest[0] + NormalizationValue, 0, MaxOutputValue)); + + Assert.Equal((int)actual, (int)expected); + } + [Theory] [InlineData(1)] [InlineData(2)] @@ -168,7 +332,7 @@ static void RunTest(string serialized) { int seed = FeatureTestRunner.Deserialize(serialized); - Span src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var block = default(Block8x8F); block.LoadFrom(src); @@ -181,14 +345,14 @@ static void RunTest(string serialized) // testee // Second transpose call is done by Quantize step // Do this manually here just to be complient to the reference implementation - FastFloatingPointDCT.TransformFDCT(ref block); + FloatingPointDCT.TransformFDCT(ref block); block.TransposeInplace(); // Part of the IDCT calculations is fused into the quantization step // We must multiply input block with adjusted no-quantization matrix // after applying FDCT Block8x8F quantMatrix = CreateBlockFromScalar(1); - FastFloatingPointDCT.AdjustToFDCT(ref quantMatrix); + FloatingPointDCT.AdjustToFDCT(ref quantMatrix); block.MultiplyInPlace(ref quantMatrix); float[] actualDest = block.ToArray(); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs index 7bcf8ab195..50c2a08bbd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs @@ -53,7 +53,7 @@ public void LLM_CalcConstants() [InlineData(2, 200)] public void LLM_IDCT_IsEquivalentTo_AccurateImplementation(int seed, int range) { - float[] sourceArray = Create8x8RoundedRandomFloatData(-range, range, seed); + float[] sourceArray = Create8x8RandomFloatData(-range, range, seed); var source = Block8x8F.Load(sourceArray); @@ -64,23 +64,6 @@ public void LLM_IDCT_IsEquivalentTo_AccurateImplementation(int seed, int range) this.CompareBlocks(expected, actual, 0.1f); } - [Theory] - [InlineData(42, 1000)] - [InlineData(1, 1000)] - [InlineData(2, 1000)] - public void LLM_IDCT_CompareToIntegerRoundedAccurateImplementation(int seed, int range) - { - Block8x8F fSource = CreateRoundedRandomFloatBlock(-range, range, seed); - Block8x8 iSource = fSource.RoundAsInt16Block(); - - Block8x8 iExpected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref iSource); - Block8x8F fExpected = iExpected.AsFloatBlock(); - - Block8x8F fActual = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref fSource); - - this.CompareBlocks(fExpected, fActual, 2); - } - [Theory] [InlineData(42)] [InlineData(1)] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs new file mode 100644 index 0000000000..008ca20c3f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class SpectralConverterTests + { + // Test for null target size, i.e. when no scaling is needed + [Theory] + [InlineData(1, 1)] + [InlineData(800, 400)] + [InlineData(2354, 4847)] + public void CalculateResultingImageSize_Null_TargetSize(int width, int height) + { + Size inputSize = new(width, height); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, null, out int blockPixelSize); + + Assert.Equal(expected: 8, blockPixelSize); + Assert.Equal(inputSize, outputSize); + } + + // Test for 'perfect' dimensions, i.e. dimensions divisible by 8, with exact scaled size match + [Theory] + [InlineData(800, 400, 800, 400, 8)] + [InlineData(800, 400, 400, 200, 4)] + [InlineData(800, 400, 200, 100, 2)] + [InlineData(800, 400, 100, 50, 1)] + public void CalculateResultingImageSize_Perfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(outputSize, targetSize); + } + + // Test for 'imperfect' dimensions, i.e. dimensions NOT divisible by 8, with exact scaled size match + [Theory] + [InlineData(7, 14, 7, 14, 8)] + [InlineData(7, 14, 4, 7, 4)] + [InlineData(7, 14, 2, 4, 2)] + [InlineData(7, 14, 1, 2, 1)] + public void CalculateResultingImageSize_Imperfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(outputSize, targetSize); + } + + // Test for inexact target and output sizes match + [Theory] + [InlineData(7, 14, 4, 6, 4, 7, 4)] + [InlineData(7, 14, 1, 1, 1, 2, 1)] + [InlineData(800, 400, 999, 600, 800, 400, 8)] + [InlineData(800, 400, 390, 150, 400, 200, 4)] + [InlineData(804, 1198, 500, 800, 804, 1198, 8)] + public void CalculateResultingImageSize_Inexact_Target_Size(int inW, int inH, int tW, int tH, int exW, int exH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + Size expectedSize = new(exW, exH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(expectedSize, outputSize); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 17efbe0b66..9824315fff 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -141,6 +141,8 @@ private class DebugSpectralConverter : SpectralConverter { private JpegFrame frame; + private IRawJpegData jpegData; + private LibJpegTools.SpectralData spectralData; private int baselineScanRowCounter; @@ -153,6 +155,7 @@ public LibJpegTools.SpectralData SpectralData // Progressive and multi-scan images must be loaded manually if (this.frame.Progressive || this.frame.MultiScan) { + this.PrepareForDecoding(); LibJpegTools.ComponentData[] components = this.spectralData.Components; for (int i = 0; i < components.Length; i++) { @@ -190,11 +193,15 @@ public override void ConvertStrideBaseline() public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { this.frame = frame; + this.jpegData = jpegData; + } - var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount]; + public override void PrepareForDecoding() + { + var spectralComponents = new LibJpegTools.ComponentData[this.frame.ComponentCount]; for (int i = 0; i < spectralComponents.Length; i++) { - var component = frame.Components[i] as JpegComponent; + JpegComponent component = this.frame.Components[i]; spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 121e1ed2fe..ef74549d0c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -89,33 +89,28 @@ public static int[] Create8x8RandomIntData(int minValue, int maxValue, int seed return result; } - internal static float[] Create8x8RoundedRandomFloatData(int minValue, int maxValue, int seed = 42) - => Create8x8RandomIntData(minValue, maxValue, seed).ConvertAllToFloat(); - - public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42) + public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8) { var rnd = new Random(seed); - var result = new float[64]; - for (int i = 0; i < 8; i++) + float[] result = new float[64]; + for (int y = 0; y < yBorder; y++) { - for (int j = 0; j < 8; j++) + int y8 = y * 8; + for (int x = 0; x < xBorder; x++) { double val = rnd.NextDouble(); val *= maxValue - minValue; val += minValue; - result[(i * 8) + j] = (float)val; + result[y8 + x] = (float)val; } } return result; } - internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42) => - Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed)); - - internal static Block8x8F CreateRoundedRandomFloatBlock(int minValue, int maxValue, int seed = 42) => - Block8x8F.Load(Create8x8RoundedRandomFloatData(minValue, maxValue, seed)); + internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8) => + Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed, xBorder, yBorder)); internal void Print8x8Data(T[] data) => this.Print8x8Data(new Span(data));