Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
153 changes: 144 additions & 9 deletions src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,126 @@ ref Unsafe.Add(ref blockRef, k),
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="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeDcScan(Component component, int restartInterval, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;

ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];

int restarts = 0;
int restartsToGo = 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 (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 (restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = restartInterval;
restarts++;
restarts &= 7;
}

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="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeAcScan(Component component, nint start, nint end, int restartInterval, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;

int restarts = 0;
int restartsToGo = 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 (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 (restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = restartInterval;
restarts++;
restarts &= 7;
}

restartsToGo--;
}
}
}

this.FlushRemainingBytes();
}

/// <summary>
/// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors.
/// </summary>
Expand Down Expand Up @@ -371,25 +491,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 +537,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)]);

/// <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