diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index bbd2bff53b..56e0f1e985 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -119,6 +119,8 @@ public void ParseEntropyCodedData(int scanComponentCount)
this.frame.AllocateComponents();
+ this.todo = this.restartInterval;
+
if (!this.frame.Progressive)
{
this.ParseBaselineData();
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index ac527ff312..90e16f6dff 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
///
private readonly byte[] streamWriteBuffer;
+ private readonly int restartInterval;
+
///
/// Number of jagged bits stored in
///
@@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the class.
///
/// Amount of encoded 8x8 blocks per single jpeg macroblock.
+ /// Numbers of MCUs between restart markers.
/// Output stream for saving encoded data.
- public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream)
+ public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
{
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
this.emitWriteIndex = this.emitBuffer.Length;
+ this.restartInterval = restartInterval;
+
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
this.target = outputStream;
@@ -211,6 +216,9 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -221,6 +229,13 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati
for (nuint k = 0; k < (uint)w; k++)
{
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ component.DcPredictor = 0;
+ }
+
this.WriteBlock(
component,
ref Unsafe.Add(ref blockRef, k),
@@ -231,6 +246,133 @@ ref Unsafe.Add(ref blockRef, k),
{
this.FlushToStream();
}
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
+ }
+ }
+
+ 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];
+
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
+ 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++)
+ {
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ component.DcPredictor = 0;
+ }
+
+ this.WriteDc(
+ component,
+ ref Unsafe.Add(ref blockRef, k),
+ ref dcHuffmanTable);
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
+ }
+ }
+
+ 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;
+
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
+ 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++)
+ {
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ }
+
+ this.WriteAcBlock(
+ ref Unsafe.Add(ref blockRef, k),
+ start,
+ end,
+ ref acHuffmanTable);
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
}
}
@@ -250,6 +392,9 @@ private void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConv
int mcusPerColumn = frame.McusPerColumn;
int mcusPerLine = frame.McusPerLine;
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
for (int j = 0; j < mcusPerColumn; j++)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -260,6 +405,16 @@ private void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConv
// Encode spectral to binary
for (int i = 0; i < mcusPerLine; i++)
{
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ foreach (var component in frame.Components)
+ {
+ component.DcPredictor = 0;
+ }
+ }
+
// Scan an interleaved mcu... process components in order
int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < frame.Components.Length; k++)
@@ -300,6 +455,17 @@ ref Unsafe.Add(ref blockRef, blockCol),
{
this.FlushToStream();
}
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
}
}
@@ -371,25 +537,29 @@ ref Unsafe.Add(ref c2BlockRef, i),
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;
- nint lastValuableIndex = block.GetLastNonZeroIndex();
-
int runLength = 0;
ref short blockRef = ref Unsafe.As(ref block);
- for (nint zig = 1; zig <= lastValuableIndex; zig++)
+ for (nint zig = start; zig < end; zig++)
{
const int zeroRun1 = 1 << 4;
const int zeroRun16 = 16 << 4;
@@ -413,14 +583,25 @@ private void WriteBlock(
}
// 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)
+ 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);
+ this.WriteAcBlock(ref block, 1, 64, ref acTable);
+ }
+
+ private void WriteRestart(int restart) =>
+ this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
+
///
/// Emits the most significant count of bits to the buffer.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
index 0daaae112c..69f04f1dcf 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
@@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
///
private int? quality;
+ ///
+ /// Backing field for
+ ///
+ private int progressiveScans = 4;
+
+ ///
+ /// Backing field for
+ ///
+ private int restartInterval;
+
///
/// 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 +43,56 @@ public int? Quality
}
}
+ ///
+ /// 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 numbers of MCUs between restart markers.
+ /// Defaults to 0.
+ ///
+ ///
+ /// Currently supported in progressive encoding only.
+ ///
+ /// Restart interval must be in [0..65535] range.
+ public int RestartInterval
+ {
+ get => this.restartInterval;
+ init
+ {
+ if (value is < 0 or > 65535)
+ {
+ throw new ArgumentException("Restart interval must be in [0..65535] range.");
+ }
+
+ this.restartInterval = value;
+ }
+ }
+
///
/// Gets the component encoding mode.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index a6ff623660..34028c2f83 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -100,12 +100,15 @@ public void Encode(Image image, Stream stream, CancellationToken
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables.
- HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
+ HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer);
+ // Write define restart interval
+ this.WriteDri(this.encoder.RestartInterval, buffer);
+
// Write scans with actual pixel data
using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
@@ -426,6 +429,25 @@ private void WriteXmpProfile(XmpProfile xmpProfile, Span buffer)
}
}
+ ///
+ /// Writes the DRI marker
+ ///
+ /// Numbers of MCUs between restart markers.
+ /// Temporary buffer.
+ private void WriteDri(int restartInterval, Span buffer)
+ {
+ if (restartInterval <= 0)
+ {
+ return;
+ }
+
+ this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);
+
+ buffer[1] = (byte)(restartInterval & 0xff);
+ buffer[0] = (byte)(restartInterval >> 8);
+ this.outputStream.Write(buffer, 0, 2);
+ }
+
///
/// Writes the App1 header.
///
@@ -563,7 +585,8 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa
// 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);
@@ -597,7 +620,17 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa
///
/// 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",
@@ -630,8 +663,8 @@ private void WriteStartOfScan(Span components, Span b
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);
}
@@ -666,7 +699,14 @@ private void WriteHuffmanScans(
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- if (frame.Components.Length == 1)
+ if (this.encoder.Progressive)
+ {
+ frame.AllocateComponents(fullScan: true);
+ spectralConverter.ConvertFull();
+
+ this.WriteProgressiveScans(frame, frameConfig, encoder, buffer, cancellationToken);
+ }
+ else if (frame.Components.Length == 1)
{
frame.AllocateComponents(fullScan: false);
@@ -694,6 +734,50 @@ private void WriteHuffmanScans(
}
}
+ ///
+ /// Writes the progressive scans
+ ///
+ /// The type of pixel format.
+ /// The current frame.
+ /// The frame configuration.
+ /// The scan encoder.
+ /// Temporary buffer.
+ /// The cancellation token.
+ private void WriteProgressiveScans(
+ JpegFrame frame,
+ JpegFrameConfig frameConfig,
+ HuffmanScanEncoder encoder,
+ Span buffer,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ 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.
///
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 1f4b3e4656..58b437af0f 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
@@ -160,6 +160,64 @@ public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvid
TestJpegEncoderCore(provider, colorType, 100, comparer);
}
+ [Theory]
+ [WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
+ [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
+ public void EncodeProgressive_DefaultNumberOfScans(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+
+ JpegEncoder encoder = new()
+ {
+ Quality = quality,
+ ColorType = colorType,
+ Progressive = true
+ };
+ string info = $"{colorType}-Q{quality}";
+
+ ImageComparer comparer = new TolerantImageComparer(tolerance);
+
+ // Does DebugSave & load reference CompareToReferenceInput():
+ image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
+ [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
+ [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
+ public void EncodeProgressive_CustomNumberOfScans(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance)
+where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+
+ JpegEncoder encoder = new()
+ {
+ Quality = quality,
+ ColorType = colorType,
+ Progressive = true,
+ ProgressiveScans = 4,
+ RestartInterval = 7
+ };
+ string info = $"{colorType}-Q{quality}";
+
+ using MemoryStream ms = new();
+ image.SaveAsJpeg(ms, encoder);
+ ms.Position = 0;
+
+ // TEMP: Save decoded output as PNG so we can do a pixel compare.
+ using Image image2 = Image.Load(ms);
+ image2.DebugSave(provider, testOutputDetails: info, extension: "png");
+
+ ImageComparer comparer = new TolerantImageComparer(tolerance);
+ image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
+ }
+
[Theory]
[InlineData(JpegColorType.YCbCrRatio420)]
[InlineData(JpegColorType.YCbCrRatio444)]