Browse Source

Add progressive JPEG encoder

pull/2740/head
Alexandr Ivanov 2 years ago
parent
commit
a059f4e9bd
  1. 130
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  2. 33
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  3. 77
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

130
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -237,6 +237,82 @@ internal class HuffmanScanEncoder
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];
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++)
{
this.WriteDc(
component,
ref Unsafe.Add(ref blockRef, k),
ref dcHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
}
}
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;
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++)
{
this.WriteAcBlock(
ref Unsafe.Add(ref blockRef, k),
start,
end,
ref acHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors.
/// </summary>
@ -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<Block8x8, short>(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;

33
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -13,6 +13,11 @@ public sealed class JpegEncoder : ImageEncoder
/// </summary>
private int? quality;
/// <summary>
/// Backing field for <see cref="ProgressiveScans"/>
/// </summary>
private int progressiveScans = 4;
/// <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).
@ -33,6 +38,34 @@ public sealed class JpegEncoder : ImageEncoder
}
}
/// <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 the component encoding mode.
/// </summary>

77
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<TPixel> 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
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer)
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) =>
this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="spectralStart">Start of spectral selection</param>
/// <param name="spectralEnd">End of spectral selection</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> 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
}
}
/// <summary>
/// Writes the progressive scans
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="frame">The current frame.</param>
/// <param name="frameConfig">The frame configuration.</param>
/// <param name="spectralConverter">The spectral converter.</param>
/// <param name="encoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void WriteProgressiveScans<TPixel>(
JpegFrame frame,
JpegFrameConfig frameConfig,
SpectralConverter<TPixel> spectralConverter,
HuffmanScanEncoder encoder,
Span<byte> buffer,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
frame.AllocateComponents(fullScan: true);
spectralConverter.ConvertFull();
Span<JpegComponentConfig> 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);
}
}
}
/// <summary>
/// Writes the header for a marker with the given length.
/// </summary>

Loading…
Cancel
Save