diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index d74494f9e5..991f7ff326 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -237,6 +237,82 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes();
}
+ ///
+ /// Encodes the DC coefficients for a given component's blocks in a scan.
+ ///
+ /// The component whose DC coefficients need to be encoded.
+ /// The token to request cancellation.
+ public void EncodeDcScan(Component component, CancellationToken cancellationToken)
+ {
+ int h = component.HeightInBlocks;
+ int w = component.WidthInBlocks;
+
+ ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
+
+ for (int i = 0; i < h; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (nuint k = 0; k < (uint)w; k++)
+ {
+ this.WriteDc(
+ component,
+ ref Unsafe.Add(ref blockRef, k),
+ ref dcHuffmanTable);
+
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+ }
+ }
+
+ this.FlushRemainingBytes();
+ }
+
+ ///
+ /// Encodes the AC coefficients for a specified range of blocks in a component's scan.
+ ///
+ /// The component whose AC coefficients need to be encoded.
+ /// The starting index of the AC coefficient range to encode.
+ /// The ending index of the AC coefficient range to encode.
+ /// The token to request cancellation.
+ public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
+ {
+ int h = component.HeightInBlocks;
+ int w = component.WidthInBlocks;
+
+ ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
+
+ for (int i = 0; i < h; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (nuint k = 0; k < (uint)w; k++)
+ {
+ this.WriteAcBlock(
+ ref Unsafe.Add(ref blockRef, k),
+ start,
+ end,
+ ref acHuffmanTable);
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+ }
+ }
+
+ this.FlushRemainingBytes();
+ }
+
///
/// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors.
///
@@ -371,16 +447,64 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes();
}
- private void WriteBlock(
+ private void WriteDc(
Component component,
ref Block8x8 block,
- ref HuffmanLut dcTable,
- ref HuffmanLut acTable)
+ ref HuffmanLut dcTable)
{
// Emit the DC delta.
int dc = block[0];
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc;
+ }
+
+ private void WriteAcBlock(
+ ref Block8x8 block,
+ nint start,
+ nint end,
+ ref HuffmanLut acTable)
+ {
+ // Emit the AC components.
+ int[] acHuffTable = acTable.Values;
+
+ int runLength = 0;
+ ref short blockRef = ref Unsafe.As(ref block);
+ for (nint zig = start; zig < end; 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 (runLength > 0)
+ {
+ this.EmitHuff(acHuffTable, 0x00);
+ }
+ }
+
+ private void WriteBlock(
+ Component component,
+ ref Block8x8 block,
+ ref HuffmanLut dcTable,
+ ref HuffmanLut acTable)
+ {
+ this.WriteDc(component, ref block, ref dcTable);
// Emit the AC components.
int[] acHuffTable = acTable.Values;
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
index 5ff4b1694d..86921e73db 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
@@ -13,6 +13,11 @@ public sealed class JpegEncoder : ImageEncoder
///
private int? quality;
+ ///
+ /// Backing field for
+ ///
+ private int progressiveScans = 4;
+
///
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min).
@@ -33,6 +38,34 @@ public sealed class JpegEncoder : ImageEncoder
}
}
+ ///
+ /// Gets a value indicating whether progressive encoding is used.
+ ///
+ public bool Progressive { get; init; }
+
+ ///
+ /// Gets number of scans per component for progressive encoding.
+ /// Defaults to 4.
+ ///
+ ///
+ /// Number of scans must be between 2 and 64.
+ /// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
+ ///
+ /// Progressive scans must be in [2..64] range.
+ public int ProgressiveScans
+ {
+ get => this.progressiveScans;
+ init
+ {
+ if (value is < 2 or > 64)
+ {
+ throw new ArgumentException("Progressive scans must be in [2..64] range.");
+ }
+
+ this.progressiveScans = value;
+ }
+ }
+
///
/// Gets the component encoding mode.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index 243bbe051d..19e5e88ca6 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -108,7 +108,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
// Write scans with actual pixel data
using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables);
- this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
+ if (this.encoder.Progressive)
+ {
+ this.WriteProgressiveScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
+ }
+ else
+ {
+ this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
+ }
// Write the End Of Image marker.
this.WriteEndOfImageMarker(buffer);
@@ -569,7 +576,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
// Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length);
- this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer);
+ byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
+ this.WriteMarkerHeader(marker, markerlen, buffer);
buffer[5] = (byte)components.Length;
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
buffer[1] = (byte)(height >> 8);
@@ -603,7 +611,17 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
///
/// The collecction of component configuration items.
/// Temporary buffer.
- private void WriteStartOfScan(Span components, Span buffer)
+ private void WriteStartOfScan(Span components, Span buffer) =>
+ this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
+
+ ///
+ /// Writes the StartOfScan marker.
+ ///
+ /// The collecction of component configuration items.
+ /// Temporary buffer.
+ /// Start of spectral selection
+ /// End of spectral selection
+ private void WriteStartOfScan(Span components, Span buffer, byte spectralStart, byte spectralEnd)
{
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
@@ -636,8 +654,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
buffer[i2 + 6] = (byte)tableSelectors;
}
- buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
- buffer[sosSize] = 0x3f; // Se - End of spectral selection.
+ buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
+ buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(buffer, 0, sosSize + 2);
}
@@ -700,6 +718,55 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
}
}
+ ///
+ /// Writes the progressive scans
+ ///
+ /// The type of pixel format.
+ /// The current frame.
+ /// The frame configuration.
+ /// The spectral converter.
+ /// The scan encoder.
+ /// Temporary buffer.
+ /// The cancellation token.
+ private void WriteProgressiveScans(
+ JpegFrame frame,
+ JpegFrameConfig frameConfig,
+ SpectralConverter spectralConverter,
+ HuffmanScanEncoder encoder,
+ Span buffer,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ frame.AllocateComponents(fullScan: true);
+ spectralConverter.ConvertFull();
+
+ Span components = frameConfig.Components;
+
+ // Phase 1: DC scan
+ for (int i = 0; i < frame.Components.Length; i++)
+ {
+ this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
+
+ encoder.EncodeDcScan(frame.Components[i], cancellationToken);
+ }
+
+ // Phase 2: AC scans
+ int acScans = this.encoder.ProgressiveScans - 1;
+ int valuesPerScan = 64 / acScans;
+ for (int scan = 0; scan < acScans; scan++)
+ {
+ int start = Math.Max(1, scan * valuesPerScan);
+ int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
+
+ for (int i = 0; i < components.Length; i++)
+ {
+ this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
+
+ encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
+ }
+ }
+ }
+
///
/// Writes the header for a marker with the given length.
///