Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add progressive JPEG encoder #2740

Merged
merged 16 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple if is fine here. Libjpeg turbo does the same.

{
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
Loading