Skip to content

Commit

Permalink
Merge pull request #2740 from ardabada/progressive-jpeg-encoder
Browse files Browse the repository at this point in the history
Add progressive JPEG encoder
  • Loading branch information
JimBobSquarePants authored Oct 12, 2024
2 parents 5c28129 + 4ff51b9 commit 0919534
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ public void ParseEntropyCodedData(int scanComponentCount)

this.frame.AllocateComponents();

this.todo = this.restartInterval;

if (!this.frame.Progressive)
{
this.ParseBaselineData();
Expand Down
201 changes: 191 additions & 10 deletions src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
/// </remarks>
private readonly byte[] streamWriteBuffer;

private readonly int restartInterval;

/// <summary>
/// Number of jagged bits stored in <see cref="accumulatedBits"/>
/// </summary>
Expand All @@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class.
/// </summary>
/// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="outputStream">Output stream for saving encoded data.</param>
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;
Expand Down Expand Up @@ -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();
Expand All @@ -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),
Expand All @@ -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();
}

/// <summary>
/// Encodes the DC coefficients for a given component's blocks in a scan.
/// </summary>
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
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<Block8x8> 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();
}

/// <summary>
/// Encodes the AC coefficients for a specified range of blocks in a component's scan.
/// </summary>
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
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<Block8x8> 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--;
}
}
}

Expand All @@ -250,6 +392,9 @@ private void EncodeScanBaselineInterleaved<TPixel>(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();
Expand All @@ -260,6 +405,16 @@ private void EncodeScanBaselineInterleaved<TPixel>(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++)
Expand Down Expand Up @@ -300,6 +455,17 @@ ref Unsafe.Add(ref blockRef, blockCol),
{
this.FlushToStream();
}

if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}

restartsToGo--;
}
}
}

Expand Down Expand Up @@ -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<Block8x8, short>(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;
Expand All @@ -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);

/// <summary>
/// Emits the most significant count of bits to the buffer.
/// </summary>
Expand Down
60 changes: 60 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
/// </summary>
private int? quality;

/// <summary>
/// Backing field for <see cref="ProgressiveScans"/>
/// </summary>
private int progressiveScans = 4;

/// <summary>
/// Backing field for <see cref="RestartInterval"/>
/// </summary>
private int restartInterval;

/// <summary>
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min).
Expand All @@ -33,6 +43,56 @@ public int? Quality
}
}

/// <summary>
/// Gets a value indicating whether progressive encoding is used.
/// </summary>
public bool Progressive { get; init; }

/// <summary>
/// Gets number of scans per component for progressive encoding.
/// Defaults to <value>4</value>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentException">Progressive scans must be in [2..64] range.</exception>
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;
}
}

/// <summary>
/// Gets numbers of MCUs between restart markers.
/// Defaults to <value>0</value>.
/// </summary>
/// <remarks>
/// Currently supported in progressive encoding only.
/// </remarks>
/// <exception cref="ArgumentException">Restart interval must be in [0..65535] range.</exception>
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;
}
}

/// <summary>
/// Gets the component encoding mode.
/// </summary>
Expand Down
Loading

0 comments on commit 0919534

Please sign in to comment.