diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs index 5134dda4e0..95ac123472 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs @@ -228,22 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// /// List of component color processors. /// Row to convert - public ComponentValues(IReadOnlyList processors, int row) - { - DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); - - this.ComponentCount = processors.Count; - - this.Component0 = processors[0].GetColorBufferRowSpan(row); - - // In case of grayscale, Component1 and Component2 point to Component0 memory area - this.Component1 = this.ComponentCount > 1 ? processors[1].GetColorBufferRowSpan(row) : this.Component0; - this.Component2 = this.ComponentCount > 2 ? processors[2].GetColorBufferRowSpan(row) : this.Component0; - this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : Span.Empty; - } - - // TODO: experimental - 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/ComponentProcessors/ComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs new file mode 100644 index 0000000000..14109b796b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + 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; } + + public abstract void CopyBlocksToColorBuffer(int spectralStep); + + 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); + + 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..8cb3e1ef57 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal sealed class DirectComponentProcessor : ComponentProcessor + { + private readonly IRawJpegData rawJpeg; + + public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8) + => this.rawJpeg = rawJpeg; + + 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 dequantTable = this.rawJpeg.QuantizationTables[this.Component.QuantizationTableIndex]; + 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 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 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/DownScalingComponentProcessor8.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs new file mode 100644 index 0000000000..0bcbf60b4a --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal sealed class DownScalingComponentProcessor8 : ComponentProcessor + { + private readonly float dcDequantizer; + + public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 1) + => this.dcDequantizer = rawJpeg.QuantizationTables[component.QuantizationTableIndex][0]; + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + + 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++) + { + // get Direct current term - averaged 8x8 pixel value + float dc = blockRow[xBlock][0]; + + // dequantization + dc *= this.dcDequantizer; + + // Normalize & round + dc = (float)Math.Round(Numerics.Clamp(dc + MathF.Ceiling(maximumValue / 2), 0, maximumValue)); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + colorBufferRow[xColorBufferStart] = dc; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor.cs deleted file mode 100644 index 1236117267..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -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 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); - } - - /// - /// 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 blocksRowsPerStep = this.component.SamplingFactors.Height; - - int yBlockStart = spectralStep * blocksRowsPerStep; - - Size subSamplingDivisors = this.component.SubSamplingDivisors; - - Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex]; - 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 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 acording 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/SpectralConversion/JpegComponentPostProcessor8.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor8.cs deleted file mode 100644 index cb082f6ccb..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor8.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - /// - /// Encapsulates spectral data to rgba32 processing for one component. - /// - internal class JpegComponentPostProcessor8 : 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 component containing decoding meta information. - /// - private readonly IJpegComponent component; - - /// - /// Gets the instance containing decoding meta information. - /// - private readonly IRawJpegData rawJpeg; - - private readonly float dcDequantizer; - - /// - /// Initializes a new instance of the class. - /// - public JpegComponentPostProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) - { - // TODO: this must be a variable depending on dct scale factor - const int blockSize = 1; - - this.frame = frame; - - this.component = component; - - this.rawJpeg = rawJpeg; - - this.dcDequantizer = rawJpeg.QuantizationTables[this.component.QuantizationTableIndex][0]; - - this.blockAreaSize = this.component.SubSamplingDivisors * blockSize; - this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( - postProcessorBufferSize.Width, - postProcessorBufferSize.Height, - this.blockAreaSize.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 blocksRowsPerStep = this.component.SamplingFactors.Height; - - 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++) - { - // get DC - averaged 8x8 pixel value - float DC = blockRow[xBlock][0]; - - // dequantization - DC *= this.dcDequantizer; - - // Normalize & round - DC = (float)Math.Round(Numerics.Clamp(DC + MathF.Ceiling(maximumValue / 2), 0, maximumValue)); - - // Save to the intermediate buffer - int xColorBufferStart = xBlock * this.blockAreaSize.Width; - colorBufferRow[xColorBufferStart] = DC; - - //// 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 acording 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/SpectralConversion/ResizingSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/ResizingSpectralConverter{TPixel}.cs deleted file mode 100644 index 65cc6ea244..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/ResizingSpectralConverter{TPixel}.cs +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Linq; -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 -{ - // TODO: docs - internal class ResizingSpectralConverter : SpectralConverter, IDisposable - where TPixel : unmanaged, IPixel - { - /// - /// instance associated with current - /// decoding routine. - /// - private readonly Configuration configuration; - - /// - /// Target image size after scaled decoding. - /// - private readonly Size targetSize; - - /// - /// Jpeg component converters from decompressed spectral to color data. - /// - private JpegComponentPostProcessor8[] componentProcessors; - - /// - /// Resulting 2D pixel buffer. - /// - private Buffer2D pixelBuffer; - - /// - /// How many pixel rows are processed in one 'stride'. - /// - private int pixelRowsPerStep; - - /// - /// How many pixel rows were processed. - /// - private int pixelRowCounter; - - /// - /// Intermediate buffer of RGB components used in color conversion. - /// - private IMemoryOwner rgbBuffer; - - /// - /// Proxy buffer used in packing from RGB to target TPixel pixels. - /// - private IMemoryOwner paddedProxyPixelRow; - - /// - /// Color converter from jpeg color space to target pixel color space. - /// - private JpegColorConverterBase colorConverter; - - public ResizingSpectralConverter(Configuration configuration, Size targetSize) - { - this.configuration = configuration; - this.targetSize = targetSize; - } - - public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) - { - MemoryAllocator allocator = this.configuration.MemoryAllocator; - - (int width, int height, int scaleDenominator) = GetScaledImageDimensions(frame.PixelWidth, frame.PixelHeight, this.targetSize.Width, this.targetSize.Height); - - // iteration data - int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); - int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height); - - this.pixelBuffer = allocator.Allocate2D( - width, - height, - this.configuration.PreferContiguousImageBuffers); - - this.paddedProxyPixelRow = allocator.Allocate(width + 3); - - // single 'stride' rgba32 buffer for conversion between spectral and TPixel - this.rgbBuffer = allocator.Allocate(width * 3); - - // component processors from spectral to Rgba32 - int blockPixelSize = 8 / scaleDenominator; - this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize; - - // color converter - JpegColorConverterBase converter = this.GetColorConverter(frame, jpegData); - this.colorConverter = converter; - - int bufferWidth = majorBlockWidth * blockPixelSize; - int batchSize = converter.ElementsPerBatch; - int correctedBufferWidth = bufferWidth + (batchSize - (bufferWidth % batchSize)); - var postProcessorBufferSize = new Size(correctedBufferWidth, this.pixelRowsPerStep); - - this.componentProcessors = new JpegComponentPostProcessor8[frame.Components.Length]; - for (int i = 0; i < this.componentProcessors.Length; i++) - { - this.componentProcessors[i] = new JpegComponentPostProcessor8(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); - } - } - - 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 (JpegComponentPostProcessor8 cpp in this.componentProcessors) - { - cpp.ClearSpectralBuffers(); - } - } - - /// - /// Converts single spectral jpeg stride to color stride. - /// - /// Spectral stride index. - private void ConvertStride(int spectralStep) - { - int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); - - for (int i = 0; i < this.componentProcessors.Length; i++) - { - this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep); - } - - int width = this.pixelBuffer.Width; - - for (int yy = this.pixelRowCounter; yy < maxY; yy++) - { - int y = yy - this.pixelRowCounter; - - var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y); - - this.colorConverter.ConvertToRgbInplace(values); - values = values.Slice(0, width); // slice away Jpeg padding - - Span r = this.rgbBuffer.Slice(0, width); - Span g = this.rgbBuffer.Slice(width, width); - Span b = this.rgbBuffer.Slice(width * 2, width); - - SimdUtils.NormalizedFloatToByteSaturate(values.Component0.Slice(0, width), r); - SimdUtils.NormalizedFloatToByteSaturate(values.Component1.Slice(0, width), g); - SimdUtils.NormalizedFloatToByteSaturate(values.Component2.Slice(0, width), b); - - // PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row. - // If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row, - // pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row. - if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span destRow)) - { - PixelOperations.Instance.PackFromRgbPlanes(this.configuration, r, g, b, destRow); - } - else - { - Span proxyRow = this.paddedProxyPixelRow.GetSpan(); - PixelOperations.Instance.PackFromRgbPlanes(this.configuration, r, g, b, proxyRow); - proxyRow.Slice(0, width).CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy)); - } - } - - this.pixelRowCounter += this.pixelRowsPerStep; - } - - public override Buffer2D GetPixelBuffer(CancellationToken cancellationToken) - { - if (!this.Converted) - { - int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); - - for (int step = 0; step < steps; step++) - { - cancellationToken.ThrowIfCancellationRequested(); - this.ConvertStride(step); - } - } - - return this.pixelBuffer; - } - - public void Dispose() - { - if (this.componentProcessors != null) - { - foreach (JpegComponentPostProcessor8 cpp in this.componentProcessors) - { - cpp.Dispose(); - } - } - - this.rgbBuffer?.Dispose(); - this.paddedProxyPixelRow?.Dispose(); - } - - // TODO: docs, code formatting - private static readonly (int Num, int Denom)[] ScalingFactors = new (int, int)[] - { - /* upscaling factors */ - // (16, 8), - // (15, 8), - // (14, 8), - // (13, 8), - // (12, 8), - // (11, 8), - // (10, 8), - // (9, 8), - - /* no scaling */ - (8, 8), - - /* downscaling factors */ - // (7, 8), // 8 => 7 - // (6, 8), // 8 => 6 - // (5, 8), // 8 => 5 - // (4, 8), // 1/2 dct scaling - currently not supported - // (3, 8), // 8 => 3 - // (2, 8), // 1/4 dct scaling - currently not supported - (1, 8), // 1/8 dct scaling - }; - - /// - /// TODO: docs, code formatting - /// - /// Initial image width. - /// Initial image height. - /// Target image width. - /// Target image height. - private static (int Width, int Height, int ScaleDenominator) GetScaledImageDimensions(int iWidth, int iHeight, int tWidth, int tHeight) - { - int output_width = iWidth; - int output_height = iHeight; - int dct_scale = 8; - - for (int i = 1; i < ScalingFactors.Length; i++) - { - (int num, int denom) = ScalingFactors[i]; - int scaledw = (int)Numerics.DivideCeil((uint)(iWidth * num), (uint)denom); - int scaledh = (int)Numerics.DivideCeil((uint)(iHeight * num), (uint)denom); - - if (scaledw < tWidth || scaledh < tHeight) - { - dct_scale = 8 / ScalingFactors[i - 1].Num; - break; - } - - output_width = scaledw; - output_height = scaledh; - } - - return (output_width, output_height, dct_scale); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter{TPixel}.cs deleted file mode 100644 index c0ed4c9d84..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter{TPixel}.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Threading; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - // TODO: docs - internal abstract class SpectralConverter : SpectralConverter - where TPixel : unmanaged, IPixel - { - /// - /// Gets converted pixel buffer. - /// - /// Cancellation token. - /// Pixel buffer. - public abstract Buffer2D GetPixelBuffer(CancellationToken cancellationToken); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs similarity index 100% rename from src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter.cs rename to src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/DirectSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs similarity index 58% rename from src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/DirectSpectralConverter{TPixel}.cs rename to src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 21ce7a16d3..23ce946678 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/DirectSpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -22,9 +22,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// /// - internal class DirectSpectralConverter : SpectralConverter, IDisposable + internal class SpectralConverter : SpectralConverter, IDisposable where TPixel : unmanaged, IPixel { + /// + /// Supported scaling factors for DCT jpeg scaling. + /// + private static readonly int[] ScalingFactors = new int[] + { + // 8 => 8, no scaling + 8, + + // 8 => 1, 1/8 of the original size + 1, + }; + /// /// instance associated with current /// decoding routine. @@ -34,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// 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. @@ -67,18 +79,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private int pixelRowCounter; /// - /// Initializes a new instance of the class. + /// 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 DirectSpectralConverter(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. + /// /// /// For non-baseline interleaved jpeg this method does a 'lazy' spectral /// conversion from spectral to color. /// - public override Buffer2D GetPixelBuffer(CancellationToken cancellationToken) + /// Cancellation token. + /// Pixel buffer. + public Buffer2D GetPixelBuffer(CancellationToken cancellationToken) { if (!this.Converted) { @@ -94,52 +123,45 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder return this.pixelBuffer; } - /// - public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + /// + /// Calculates resulting image size and jpeg block scaling. + /// + /// Native size of the image. + /// Resulting jpeg block pixel size. + /// Scaled jpeg image size. + private Size GetResultingImageSize(Size nativeSize, out int blockPixelSize) { - 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++) + if (this.targetSize == null) { - this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); + blockPixelSize = 8; + return nativeSize; } + else + { + const uint jpegBlockPixelSize = 8; - // single 'stride' rgba32 buffer for conversion between spectral and TPixel - this.rgbBuffer = allocator.Allocate(frame.PixelWidth * 3); + Size targetSize = this.targetSize.Value; + int outputWidth = nativeSize.Width; + int outputHeight = nativeSize.Height; + blockPixelSize = 1; - // 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); + for (int i = 1; i < ScalingFactors.Length; i++) + { + int scale = ScalingFactors[i]; + int scaledw = (int)Numerics.DivideCeil((uint)(nativeSize.Width * scale), jpegBlockPixelSize); + int scaledh = (int)Numerics.DivideCeil((uint)(nativeSize.Height * scale), jpegBlockPixelSize); + + if (scaledw < targetSize.Width || scaledh < targetSize.Height) + { + blockPixelSize = ScalingFactors[i - 1]; + break; + } + + outputWidth = scaledw; + outputHeight = scaledh; + } - foreach (JpegComponentPostProcessor cpp in this.componentProcessors) - { - cpp.ClearSpectralBuffers(); + return new Size(outputWidth, outputHeight); } } @@ -193,12 +215,80 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.pixelRowCounter += this.pixelRowsPerStep; } + /// + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + MemoryAllocator allocator = this.configuration.MemoryAllocator; + + // color converter from RGB to TPixel + JpegColorConverterBase converter = this.GetColorConverter(frame, jpegData); + this.colorConverter = converter; + + // Resulting image size + Size pixelSize = this.GetResultingImageSize(frame.PixelSize, out int blockPixelSize); + + // iteration data + int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); + int majorVerticalSamplingFactor = 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 converterAlignedBufferWidth = bufferWidth + (batchSize - (bufferWidth % batchSize)); + var postProcessorBufferSize = new Size(converterAlignedBufferWidth, this.pixelRowsPerStep); + this.componentProcessors = new ComponentProcessor[frame.Components.Length]; + switch (blockPixelSize) + { + case 8: + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i] = new DirectComponentProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); + } + + break; + case 1: + for (int i = 0; i < this.componentProcessors.Length; i++) + { + this.componentProcessors[i] = new DownScalingComponentProcessor8(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); + } + + break; + + // TODO: default? + } + + // 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(); + } + } + /// 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/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 295569c980..bab75d15c7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -32,8 +32,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); - // TODO: this implementation is experimental - public Image experimental__DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) + /// + /// TODO: this implementation is experimental + /// + /// Placeholder1 + /// Placeholder2 + /// Placeholder3 + /// Placeholder4 + /// Placeholder5 + /// Placeholder6 + public Image Experimental__DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -45,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var bufferedReadStream = new BufferedReadStream(configuration, stream); try { - return decoder.experimental__DecodeInto(bufferedReadStream, targetSize, cancellationToken); + return decoder.Experimental__DecodeInto(bufferedReadStream, targetSize, cancellationToken); } catch (InvalidMemoryOperationException ex) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 7709a58be6..269c1f9935 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -182,6 +182,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return new JpegFileMarker(marker[1], stream.Position - 2, true); } + /// + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using var spectralConverter = new SpectralConverter(this.Configuration); + return this.Decode(stream, spectralConverter, cancellationToken); + } + + // TODO: docs + public Image Experimental__DecodeInto(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using var spectralConverter = new SpectralConverter(this.Configuration, targetSize); + return this.Decode(stream, spectralConverter, cancellationToken); + } + // TODO: docs private Image Decode(BufferedReadStream stream, SpectralConverter converter, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -201,22 +217,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Metadata); } - // TODO: docs - public Image experimental__DecodeInto(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - using var converter = new ResizingSpectralConverter(this.Configuration, targetSize); - return this.Decode(stream, converter, cancellationToken); - } - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - using var converter = new DirectSpectralConverter(this.Configuration); - return this.Decode(stream, converter, cancellationToken); - } - /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs index e084fdf16d..5b793c35de 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// Spectral converter for gray TIFF's which use the JPEG compression. /// /// The type of the pixel. - internal sealed class GrayJpegSpectralConverter : DirectSpectralConverter + internal sealed class GrayJpegSpectralConverter : SpectralConverter where TPixel : unmanaged, IPixel { /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index 87c34a8402..cfbc32f4f6 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.BlackIsZero: case TiffPhotometricInterpretation.WhiteIsZero: { - using DirectSpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None); @@ -74,8 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.YCbCr: case TiffPhotometricInterpretation.Rgb: { - using DirectSpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? - new RgbJpegSpectralConverter(this.configuration) : new DirectSpectralConverter(this.configuration); + using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? + new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoder); jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs index 3e06152462..a83518064d 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// The jpeg data should be always treated as RGB color space. /// /// The type of the pixel. - internal sealed class RgbJpegSpectralConverter : DirectSpectralConverter + internal sealed class RgbJpegSpectralConverter : SpectralConverter where TPixel : unmanaged, IPixel { /// diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs index d64eb15ae4..0b2885d654 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs @@ -79,4 +79,21 @@ Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores | '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.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs index 322c044bb6..27240831c3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); // Decoding - using var converter = new DirectSpectralConverter(Configuration.Default); + using var converter = new SpectralConverter(Configuration.Default); using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default); decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);