Skip to content

Commit a059f4e

Browse files
committed
Add progressive JPEG encoder
1 parent 77a5883 commit a059f4e

File tree

3 files changed

+232
-8
lines changed

3 files changed

+232
-8
lines changed

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

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,82 @@ ref Unsafe.Add(ref blockRef, k),
237237
this.FlushRemainingBytes();
238238
}
239239

240+
/// <summary>
241+
/// Encodes the DC coefficients for a given component's blocks in a scan.
242+
/// </summary>
243+
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
244+
/// <param name="cancellationToken">The token to request cancellation.</param>
245+
public void EncodeDcScan(Component component, CancellationToken cancellationToken)
246+
{
247+
int h = component.HeightInBlocks;
248+
int w = component.WidthInBlocks;
249+
250+
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
251+
252+
for (int i = 0; i < h; i++)
253+
{
254+
cancellationToken.ThrowIfCancellationRequested();
255+
256+
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
257+
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
258+
259+
for (nuint k = 0; k < (uint)w; k++)
260+
{
261+
this.WriteDc(
262+
component,
263+
ref Unsafe.Add(ref blockRef, k),
264+
ref dcHuffmanTable);
265+
266+
267+
if (this.IsStreamFlushNeeded)
268+
{
269+
this.FlushToStream();
270+
}
271+
}
272+
}
273+
274+
this.FlushRemainingBytes();
275+
}
276+
277+
/// <summary>
278+
/// Encodes the AC coefficients for a specified range of blocks in a component's scan.
279+
/// </summary>
280+
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
281+
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
282+
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
283+
/// <param name="cancellationToken">The token to request cancellation.</param>
284+
public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
285+
{
286+
int h = component.HeightInBlocks;
287+
int w = component.WidthInBlocks;
288+
289+
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
290+
291+
for (int i = 0; i < h; i++)
292+
{
293+
cancellationToken.ThrowIfCancellationRequested();
294+
295+
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
296+
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
297+
298+
for (nuint k = 0; k < (uint)w; k++)
299+
{
300+
this.WriteAcBlock(
301+
ref Unsafe.Add(ref blockRef, k),
302+
start,
303+
end,
304+
ref acHuffmanTable);
305+
306+
if (this.IsStreamFlushNeeded)
307+
{
308+
this.FlushToStream();
309+
}
310+
}
311+
}
312+
313+
this.FlushRemainingBytes();
314+
}
315+
240316
/// <summary>
241317
/// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors.
242318
/// </summary>
@@ -371,16 +447,64 @@ ref Unsafe.Add(ref c2BlockRef, i),
371447
this.FlushRemainingBytes();
372448
}
373449

374-
private void WriteBlock(
450+
private void WriteDc(
375451
Component component,
376452
ref Block8x8 block,
377-
ref HuffmanLut dcTable,
378-
ref HuffmanLut acTable)
453+
ref HuffmanLut dcTable)
379454
{
380455
// Emit the DC delta.
381456
int dc = block[0];
382457
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
383458
component.DcPredictor = dc;
459+
}
460+
461+
private void WriteAcBlock(
462+
ref Block8x8 block,
463+
nint start,
464+
nint end,
465+
ref HuffmanLut acTable)
466+
{
467+
// Emit the AC components.
468+
int[] acHuffTable = acTable.Values;
469+
470+
int runLength = 0;
471+
ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block);
472+
for (nint zig = start; zig < end; zig++)
473+
{
474+
const int zeroRun1 = 1 << 4;
475+
const int zeroRun16 = 16 << 4;
476+
477+
int ac = Unsafe.Add(ref blockRef, zig);
478+
if (ac == 0)
479+
{
480+
runLength += zeroRun1;
481+
}
482+
else
483+
{
484+
while (runLength >= zeroRun16)
485+
{
486+
this.EmitHuff(acHuffTable, 0xf0);
487+
runLength -= zeroRun16;
488+
}
489+
490+
this.EmitHuffRLE(acHuffTable, runLength, ac);
491+
runLength = 0;
492+
}
493+
}
494+
495+
if (runLength > 0)
496+
{
497+
this.EmitHuff(acHuffTable, 0x00);
498+
}
499+
}
500+
501+
private void WriteBlock(
502+
Component component,
503+
ref Block8x8 block,
504+
ref HuffmanLut dcTable,
505+
ref HuffmanLut acTable)
506+
{
507+
this.WriteDc(component, ref block, ref dcTable);
384508

385509
// Emit the AC components.
386510
int[] acHuffTable = acTable.Values;

src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public sealed class JpegEncoder : ImageEncoder
1313
/// </summary>
1414
private int? quality;
1515

16+
/// <summary>
17+
/// Backing field for <see cref="ProgressiveScans"/>
18+
/// </summary>
19+
private int progressiveScans = 4;
20+
1621
/// <summary>
1722
/// Gets the quality, that will be used to encode the image. Quality
1823
/// index must be between 1 and 100 (compression from max to min).
@@ -33,6 +38,34 @@ public int? Quality
3338
}
3439
}
3540

41+
/// <summary>
42+
/// Gets a value indicating whether progressive encoding is used.
43+
/// </summary>
44+
public bool Progressive { get; init; }
45+
46+
/// <summary>
47+
/// Gets number of scans per component for progressive encoding.
48+
/// Defaults to <value>4</value>.
49+
/// </summary>
50+
/// <remarks>
51+
/// Number of scans must be between 2 and 64.
52+
/// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
53+
/// </remarks>
54+
/// <exception cref="ArgumentException">Progressive scans must be in [2..64] range.</exception>
55+
public int ProgressiveScans
56+
{
57+
get => this.progressiveScans;
58+
init
59+
{
60+
if (value is < 2 or > 64)
61+
{
62+
throw new ArgumentException("Progressive scans must be in [2..64] range.");
63+
}
64+
65+
this.progressiveScans = value;
66+
}
67+
}
68+
3669
/// <summary>
3770
/// Gets the component encoding mode.
3871
/// </summary>

src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
108108

109109
// Write scans with actual pixel data
110110
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables);
111-
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
111+
if (this.encoder.Progressive)
112+
{
113+
this.WriteProgressiveScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
114+
}
115+
else
116+
{
117+
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
118+
}
112119

113120
// Write the End Of Image marker.
114121
this.WriteEndOfImageMarker(buffer);
@@ -569,7 +576,8 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa
569576

570577
// Length (high byte, low byte), 8 + components * 3.
571578
int markerlen = 8 + (3 * components.Length);
572-
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer);
579+
byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
580+
this.WriteMarkerHeader(marker, markerlen, buffer);
573581
buffer[5] = (byte)components.Length;
574582
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
575583
buffer[1] = (byte)(height >> 8);
@@ -603,7 +611,17 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa
603611
/// </summary>
604612
/// <param name="components">The collecction of component configuration items.</param>
605613
/// <param name="buffer">Temporary buffer.</param>
606-
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer)
614+
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) =>
615+
this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
616+
617+
/// <summary>
618+
/// Writes the StartOfScan marker.
619+
/// </summary>
620+
/// <param name="components">The collecction of component configuration items.</param>
621+
/// <param name="buffer">Temporary buffer.</param>
622+
/// <param name="spectralStart">Start of spectral selection</param>
623+
/// <param name="spectralEnd">End of spectral selection</param>
624+
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer, byte spectralStart, byte spectralEnd)
607625
{
608626
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
609627
// - the marker length "\x00\x0c",
@@ -636,8 +654,8 @@ private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> b
636654
buffer[i2 + 6] = (byte)tableSelectors;
637655
}
638656

639-
buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
640-
buffer[sosSize] = 0x3f; // Se - End of spectral selection.
657+
buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
658+
buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
641659
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
642660
this.outputStream.Write(buffer, 0, sosSize + 2);
643661
}
@@ -700,6 +718,55 @@ private void WriteHuffmanScans<TPixel>(
700718
}
701719
}
702720

721+
/// <summary>
722+
/// Writes the progressive scans
723+
/// </summary>
724+
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
725+
/// <param name="frame">The current frame.</param>
726+
/// <param name="frameConfig">The frame configuration.</param>
727+
/// <param name="spectralConverter">The spectral converter.</param>
728+
/// <param name="encoder">The scan encoder.</param>
729+
/// <param name="buffer">Temporary buffer.</param>
730+
/// <param name="cancellationToken">The cancellation token.</param>
731+
private void WriteProgressiveScans<TPixel>(
732+
JpegFrame frame,
733+
JpegFrameConfig frameConfig,
734+
SpectralConverter<TPixel> spectralConverter,
735+
HuffmanScanEncoder encoder,
736+
Span<byte> buffer,
737+
CancellationToken cancellationToken)
738+
where TPixel : unmanaged, IPixel<TPixel>
739+
{
740+
frame.AllocateComponents(fullScan: true);
741+
spectralConverter.ConvertFull();
742+
743+
Span<JpegComponentConfig> components = frameConfig.Components;
744+
745+
// Phase 1: DC scan
746+
for (int i = 0; i < frame.Components.Length; i++)
747+
{
748+
this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
749+
750+
encoder.EncodeDcScan(frame.Components[i], cancellationToken);
751+
}
752+
753+
// Phase 2: AC scans
754+
int acScans = this.encoder.ProgressiveScans - 1;
755+
int valuesPerScan = 64 / acScans;
756+
for (int scan = 0; scan < acScans; scan++)
757+
{
758+
int start = Math.Max(1, scan * valuesPerScan);
759+
int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
760+
761+
for (int i = 0; i < components.Length; i++)
762+
{
763+
this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
764+
765+
encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
766+
}
767+
}
768+
}
769+
703770
/// <summary>
704771
/// Writes the header for a marker with the given length.
705772
/// </summary>

0 commit comments

Comments
 (0)