diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
index acd98bcfcd..9901639a79 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
@@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
///
- /// Converter used to convert jpeg spectral data to color pixels.
+ /// Converter used to convert jpeg spectral data to pixels.
///
internal abstract class SpectralConverter
{
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index 6acc6b6db0..7c683af2b3 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -128,6 +128,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
get => this.emitWriteIndex < (uint)this.emitBuffer.Length / 2;
}
+ public void Encode(Image image, Block8x8F[] quantTables, Configuration configuration, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ // DEBUG INITIALIZATION SETUP
+ var frame = new JpegFrame(configuration.MemoryAllocator, image, componentCount: 3);
+ frame.Init(1, 1);
+ frame.AllocateComponents(fullScan: false);
+
+ var spectralConverter = new SpectralConverter(configuration);
+ spectralConverter.InjectFrameData(frame, image, quantTables);
+
+ // DEBUG ENCODING SETUP
+ int mcu = 0;
+ int mcusPerColumn = frame.McusPerColumn;
+ int mcusPerLine = frame.McusPerLine;
+
+ for (int j = 0; j < mcusPerColumn; j++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Convert from pixels to spectral via given converter
+ spectralConverter.ConvertStrideBaseline();
+
+ // decode from binary to spectral
+ for (int i = 0; i < mcusPerLine; i++)
+ {
+ // Scan an interleaved mcu... process components in order
+ int mcuCol = mcu % mcusPerLine;
+ for (int k = 0; k < frame.ComponentCount; k++)
+ {
+ JpegComponent component = frame.Components[k];
+
+ ref HuffmanLut dcHuffmanTable = ref this.huffmanTables[component.DcTableId];
+ ref HuffmanLut acHuffmanTable = ref this.huffmanTables[component.AcTableId];
+
+ int h = component.HorizontalSamplingFactor;
+ int v = component.VerticalSamplingFactor;
+
+ // Scan out an mcu's worth of this component; that's just determined
+ // by the basic H and V specified for the component
+ for (int y = 0; y < v; y++)
+ {
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (int x = 0; x < h; x++)
+ {
+ int blockCol = (mcuCol * h) + x;
+
+ this.WriteBlock(
+ component,
+ ref Unsafe.Add(ref blockRef, blockCol),
+ ref dcHuffmanTable,
+ ref acHuffmanTable);
+ }
+ }
+ }
+
+ // After all interleaved components, that's an interleaved MCU
+ mcu++;
+ }
+ }
+
+ this.FlushRemainingBytes();
+ }
+
///
/// Encodes the image with no subsampling.
///
@@ -441,6 +507,56 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
return dc;
}
+ private void WriteBlock(
+ JpegComponent component,
+ ref Block8x8 block,
+ ref HuffmanLut dcTable,
+ ref HuffmanLut acTable)
+ {
+ // Emit the DC delta.
+ int dc = block[0];
+ this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
+ component.DcPredictor = dc;
+
+ // Emit the AC components.
+ int[] acHuffTable = acTable.Values;
+
+ nint lastValuableIndex = block.GetLastNonZeroIndex();
+
+ int runLength = 0;
+ ref short blockRef = ref Unsafe.As(ref block);
+ for (nint zig = 1; zig <= lastValuableIndex; zig++)
+ {
+ const int zeroRun1 = 1 << 4;
+ const int zeroRun16 = 16 << 4;
+
+ int ac = Unsafe.Add(ref blockRef, zig);
+ if (ac == 0)
+ {
+ runLength += zeroRun1;
+ }
+ else
+ {
+ while (runLength >= zeroRun16)
+ {
+ this.EmitHuff(acHuffTable, 0xf0);
+ runLength -= zeroRun16;
+ }
+
+ this.EmitHuffRLE(acHuffTable, runLength, ac);
+ runLength = 0;
+ }
+ }
+
+ // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over
+ // this can be done for any number of trailing zeros, even when all 63 ac values are zero
+ // (Block8x8F.Size - 1) == 63 - last index of the mcu elements
+ if (lastValuableIndex != Block8x8F.Size - 1)
+ {
+ this.EmitHuff(acHuffTable, 0x00);
+ }
+ }
+
///
/// Emits the most significant count of bits to the buffer.
///
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs
new file mode 100644
index 0000000000..3293f55d1e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+ ///
+ /// Represents a single frame component.
+ ///
+ internal class JpegComponent : IDisposable
+ {
+ private readonly MemoryAllocator memoryAllocator;
+
+ public JpegComponent(MemoryAllocator memoryAllocator, int horizontalFactor, int verticalFactor, byte quantizationTableIndex)
+ {
+ this.memoryAllocator = memoryAllocator;
+
+ this.HorizontalSamplingFactor = horizontalFactor;
+ this.VerticalSamplingFactor = verticalFactor;
+ this.SamplingFactors = new Size(this.HorizontalSamplingFactor, this.VerticalSamplingFactor);
+
+ this.QuantizationTableIndex = quantizationTableIndex;
+ }
+
+ ///
+ /// Gets or sets DC coefficient predictor.
+ ///
+ public int DcPredictor { get; set; }
+
+ ///
+ /// Gets the horizontal sampling factor.
+ ///
+ public int HorizontalSamplingFactor { get; }
+
+ ///
+ /// Gets the vertical sampling factor.
+ ///
+ public int VerticalSamplingFactor { get; }
+
+ ///
+ public Buffer2D SpectralBlocks { get; private set; }
+
+ ///
+ public Size SubSamplingDivisors { get; private set; }
+
+ ///
+ public int QuantizationTableIndex { get; }
+
+ ///
+ public Size SizeInBlocks { get; private set; }
+
+ ///
+ public Size SamplingFactors { get; set; }
+
+ ///
+ /// Gets the number of blocks per line.
+ ///
+ public int WidthInBlocks { get; private set; }
+
+ ///
+ /// Gets the number of blocks per column.
+ ///
+ public int HeightInBlocks { get; private set; }
+
+ ///
+ /// Gets or sets the index for the DC Huffman table.
+ ///
+ public int DcTableId { get; set; }
+
+ ///
+ /// Gets or sets the index for the AC Huffman table.
+ ///
+ public int AcTableId { get; set; }
+
+ ///
+ public void Dispose()
+ {
+ this.SpectralBlocks?.Dispose();
+ this.SpectralBlocks = null;
+ }
+
+ ///
+ /// Initializes component for future buffers initialization.
+ ///
+ /// Maximal horizontal subsampling factor among all the components.
+ /// Maximal vertical subsampling factor among all the components.
+ public void Init(JpegFrame frame, int maxSubFactorH, int maxSubFactorV)
+ {
+ this.WidthInBlocks = (int)MathF.Ceiling(
+ MathF.Ceiling(frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / maxSubFactorH);
+
+ this.HeightInBlocks = (int)MathF.Ceiling(
+ MathF.Ceiling(frame.PixelHeight / 8F) * this.VerticalSamplingFactor / maxSubFactorV);
+
+ int blocksPerLineForMcu = frame.McusPerLine * this.HorizontalSamplingFactor;
+ int blocksPerColumnForMcu = frame.McusPerColumn * this.VerticalSamplingFactor;
+ this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu);
+
+ this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors);
+
+ if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0)
+ {
+ JpegThrowHelper.ThrowBadSampling();
+ }
+ }
+
+ public void AllocateSpectral(bool fullScan)
+ {
+ int spectralAllocWidth = this.SizeInBlocks.Width;
+ int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor;
+
+ this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs
new file mode 100644
index 0000000000..dd9d485a84
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+ internal class JpegComponentPostProcessor : IDisposable
+ {
+ private readonly JpegComponent component;
+
+ private readonly Block8x8F dequantTable;
+
+ public JpegComponentPostProcessor(JpegComponent component, Block8x8F dequantTable)
+ {
+ this.component = component;
+ this.dequantTable = dequantTable;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs
new file mode 100644
index 0000000000..585e3bafaa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+ ///
+ /// Represent a single jpeg frame.
+ ///
+ internal sealed class JpegFrame : IDisposable
+ {
+ public JpegFrame(MemoryAllocator allocator, Image image, byte componentCount)
+ {
+ this.PixelWidth = image.Width;
+ this.PixelHeight = image.Height;
+
+ if (componentCount != 3)
+ {
+ throw new ArgumentException("This is YCbCr debug path only.");
+ }
+
+ this.Components = new JpegComponent[]
+ {
+ new JpegComponent(allocator, 1, 1, 0),
+ new JpegComponent(allocator, 1, 1, 1),
+ new JpegComponent(allocator, 1, 1, 1),
+ };
+ }
+
+ ///
+ /// Gets the number of pixel per row.
+ ///
+ public int PixelHeight { get; private set; }
+
+ ///
+ /// Gets the number of pixels per line.
+ ///
+ public int PixelWidth { get; private set; }
+
+ ///
+ /// Gets the number of components within a frame.
+ ///
+ public int ComponentCount => this.Components.Length;
+
+ ///
+ /// Gets the frame component collection.
+ ///
+ public JpegComponent[] Components { get; }
+
+ ///
+ /// Gets or sets the number of MCU's per line.
+ ///
+ public int McusPerLine { get; set; }
+
+ ///
+ /// Gets or sets the number of MCU's per column.
+ ///
+ public int McusPerColumn { get; set; }
+
+ ///
+ public void Dispose()
+ {
+ for (int i = 0; i < this.Components.Length; i++)
+ {
+ this.Components[i]?.Dispose();
+ }
+ }
+
+ ///
+ /// Allocates the frame component blocks.
+ ///
+ /// Maximal horizontal subsampling factor among all the components.
+ /// Maximal vertical subsampling factor among all the components.
+ public void Init(int maxSubFactorH, int maxSubFactorV)
+ {
+ this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8);
+ this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8);
+
+ for (int i = 0; i < this.ComponentCount; i++)
+ {
+ JpegComponent component = this.Components[i];
+ component.Init(this, maxSubFactorH, maxSubFactorV);
+ }
+ }
+
+ public void AllocateComponents(bool fullScan)
+ {
+ for (int i = 0; i < this.ComponentCount; i++)
+ {
+ JpegComponent component = this.Components[i];
+ component.AllocateSpectral(fullScan);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs
new file mode 100644
index 0000000000..b85cf1d371
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+ ///
+ /// Converter used to convert pixel data to jpeg spectral data.
+ ///
+ internal abstract class SpectralConverter
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
new file mode 100644
index 0000000000..1065e69c56
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Linq;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+
+ ///
+ internal class SpectralConverter : SpectralConverter
+ where TPixel : unmanaged, IPixel
+ {
+ private readonly Configuration configuration;
+
+ private JpegComponentPostProcessor[] componentProcessors;
+
+ private int pixelRowsPerStep;
+
+ private int pixelRowCounter;
+
+ private IMemoryOwner rgbBuffer;
+
+ private IMemoryOwner paddedProxyPixelRow;
+
+ private Buffer2D pixelBuffer;
+
+ public SpectralConverter(Configuration configuration) =>
+ this.configuration = configuration;
+
+ public void InjectFrameData(JpegFrame frame, Image image, Block8x8F[] dequantTables)
+ {
+ 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 of the image
+ // currently codec only supports encoding single frame jpegs
+ this.pixelBuffer = image.GetRootFramePixelBuffer();
+
+ // ???
+ //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++)
+ {
+ JpegComponent component = frame.Components[i];
+ this.componentProcessors[i] = new JpegComponentPostProcessor(component, dequantTables[component.QuantizationTableIndex]);
+ }
+
+ // 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 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);
+ }
+
+ private void ConvertStride(int spectralStep)
+ {
+ // 1. Unpack from TPixel to r/g/b planes
+ // 2. Byte r/g/b planes to normalized float r/g/b planes
+ // 3. Convert from r/g/b planes to target pixel type with JpegColorConverter
+ // 4. Convert color buffer to spectral blocks with component post processors
+
+ 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, r);
+ // SimdUtils.NormalizedFloatToByteSaturate(values.Component1, g);
+ // SimdUtils.NormalizedFloatToByteSaturate(values.Component2, 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;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index a3cff8f31d..c20babd3d5 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -131,25 +131,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Write the scan header.
this.WriteStartOfScan(componentCount, componentIds);
+ var quantTables = new Block8x8F[] { luminanceQuantTable, chrominanceQuantTable };
+ new HuffmanScanEncoder(3, stream).Encode(image, quantTables, Configuration.Default, cancellationToken);
+
// Write the scan compressed data.
- switch (this.colorType)
- {
- case JpegColorType.YCbCrRatio444:
- new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
- break;
- case JpegColorType.YCbCrRatio420:
- new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
- break;
- case JpegColorType.Luminance:
- new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
- break;
- case JpegColorType.Rgb:
- new HuffmanScanEncoder(3, stream).EncodeRgb(image, ref luminanceQuantTable, cancellationToken);
- break;
- default:
- // all other non-supported color types are checked at the start of this method
- break;
- }
+ //switch (this.colorType)
+ //{
+ // case JpegColorType.YCbCrRatio444:
+ // new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
+ // break;
+ // case JpegColorType.YCbCrRatio420:
+ // new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
+ // break;
+ // case JpegColorType.Luminance:
+ // new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
+ // break;
+ // case JpegColorType.Rgb:
+ // new HuffmanScanEncoder(3, stream).EncodeRgb(image, ref luminanceQuantTable, cancellationToken);
+ // break;
+ // default:
+ // // all other non-supported color types are checked at the start of this method
+ // break;
+ //}
// Write the End Of Image marker.
this.WriteEndOfImageMarker();