Skip to content

Commit a34bca3

Browse files
committed
Update encoder and decoder to handle restart interval
1 parent aa77441 commit a34bca3

File tree

4 files changed

+101
-20
lines changed

4 files changed

+101
-20
lines changed

src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ public void ParseEntropyCodedData(int scanComponentCount)
119119

120120
this.frame.AllocateComponents();
121121

122+
this.todo = this.restartInterval;
123+
122124
if (!this.frame.Progressive)
123125
{
124126
this.ParseBaselineData();

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

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
8787
/// </remarks>
8888
private readonly byte[] streamWriteBuffer;
8989

90+
private readonly int restartInterval;
91+
9092
/// <summary>
9193
/// Number of jagged bits stored in <see cref="accumulatedBits"/>
9294
/// </summary>
@@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
103105
/// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class.
104106
/// </summary>
105107
/// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param>
108+
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
106109
/// <param name="outputStream">Output stream for saving encoded data.</param>
107-
public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream)
110+
public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
108111
{
109112
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
110113
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
111114
this.emitWriteIndex = this.emitBuffer.Length;
112115

116+
this.restartInterval = restartInterval;
117+
113118
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
114119

115120
this.target = outputStream;
@@ -211,6 +216,9 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati
211216
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
212217
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
213218

219+
int restarts = 0;
220+
int restartsToGo = this.restartInterval;
221+
214222
for (int i = 0; i < h; i++)
215223
{
216224
cancellationToken.ThrowIfCancellationRequested();
@@ -221,6 +229,13 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati
221229

222230
for (nuint k = 0; k < (uint)w; k++)
223231
{
232+
if (this.restartInterval > 0 && restartsToGo == 0)
233+
{
234+
this.FlushRemainingBytes();
235+
this.WriteRestart(restarts % 8);
236+
component.DcPredictor = 0;
237+
}
238+
224239
this.WriteBlock(
225240
component,
226241
ref Unsafe.Add(ref blockRef, k),
@@ -231,6 +246,17 @@ ref Unsafe.Add(ref blockRef, k),
231246
{
232247
this.FlushToStream();
233248
}
249+
250+
if (this.restartInterval > 0)
251+
{
252+
if (restartsToGo == 0)
253+
{
254+
restartsToGo = this.restartInterval;
255+
restarts++;
256+
}
257+
258+
restartsToGo--;
259+
}
234260
}
235261
}
236262

@@ -241,17 +267,16 @@ ref Unsafe.Add(ref blockRef, k),
241267
/// Encodes the DC coefficients for a given component's blocks in a scan.
242268
/// </summary>
243269
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
244-
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
245270
/// <param name="cancellationToken">The token to request cancellation.</param>
246-
public void EncodeDcScan(Component component, int restartInterval, CancellationToken cancellationToken)
271+
public void EncodeDcScan(Component component, CancellationToken cancellationToken)
247272
{
248273
int h = component.HeightInBlocks;
249274
int w = component.WidthInBlocks;
250275

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

253278
int restarts = 0;
254-
int restartsToGo = restartInterval;
279+
int restartsToGo = this.restartInterval;
255280

256281
for (int i = 0; i < h; i++)
257282
{
@@ -262,7 +287,7 @@ public void EncodeDcScan(Component component, int restartInterval, CancellationT
262287

263288
for (nuint k = 0; k < (uint)w; k++)
264289
{
265-
if (restartInterval > 0 && restartsToGo == 0)
290+
if (this.restartInterval > 0 && restartsToGo == 0)
266291
{
267292
this.FlushRemainingBytes();
268293
this.WriteRestart(restarts % 8);
@@ -279,13 +304,12 @@ ref Unsafe.Add(ref blockRef, k),
279304
this.FlushToStream();
280305
}
281306

282-
if (restartInterval > 0)
307+
if (this.restartInterval > 0)
283308
{
284309
if (restartsToGo == 0)
285310
{
286-
restartsToGo = restartInterval;
311+
restartsToGo = this.restartInterval;
287312
restarts++;
288-
restarts &= 7;
289313
}
290314

291315
restartsToGo--;
@@ -302,15 +326,14 @@ ref Unsafe.Add(ref blockRef, k),
302326
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
303327
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
304328
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
305-
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
306329
/// <param name="cancellationToken">The token to request cancellation.</param>
307-
public void EncodeAcScan(Component component, nint start, nint end, int restartInterval, CancellationToken cancellationToken)
330+
public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
308331
{
309332
int h = component.HeightInBlocks;
310333
int w = component.WidthInBlocks;
311334

312335
int restarts = 0;
313-
int restartsToGo = restartInterval;
336+
int restartsToGo = this.restartInterval;
314337

315338
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
316339

@@ -323,7 +346,7 @@ public void EncodeAcScan(Component component, nint start, nint end, int restartI
323346

324347
for (nuint k = 0; k < (uint)w; k++)
325348
{
326-
if (restartInterval > 0 && restartsToGo == 0)
349+
if (this.restartInterval > 0 && restartsToGo == 0)
327350
{
328351
this.FlushRemainingBytes();
329352
this.WriteRestart(restarts % 8);
@@ -340,13 +363,12 @@ ref Unsafe.Add(ref blockRef, k),
340363
this.FlushToStream();
341364
}
342365

343-
if (restartInterval > 0)
366+
if (this.restartInterval > 0)
344367
{
345368
if (restartsToGo == 0)
346369
{
347-
restartsToGo = restartInterval;
370+
restartsToGo = this.restartInterval;
348371
restarts++;
349-
restarts &= 7;
350372
}
351373

352374
restartsToGo--;
@@ -370,6 +392,9 @@ private void EncodeScanBaselineInterleaved<TPixel>(JpegFrame frame, SpectralConv
370392
int mcusPerColumn = frame.McusPerColumn;
371393
int mcusPerLine = frame.McusPerLine;
372394

395+
int restarts = 0;
396+
int restartsToGo = this.restartInterval;
397+
373398
for (int j = 0; j < mcusPerColumn; j++)
374399
{
375400
cancellationToken.ThrowIfCancellationRequested();
@@ -380,6 +405,16 @@ private void EncodeScanBaselineInterleaved<TPixel>(JpegFrame frame, SpectralConv
380405
// Encode spectral to binary
381406
for (int i = 0; i < mcusPerLine; i++)
382407
{
408+
if (this.restartInterval > 0 && restartsToGo == 0)
409+
{
410+
this.FlushRemainingBytes();
411+
this.WriteRestart(restarts % 8);
412+
foreach (var component in frame.Components)
413+
{
414+
component.DcPredictor = 0;
415+
}
416+
}
417+
383418
// Scan an interleaved mcu... process components in order
384419
int mcuCol = mcu % mcusPerLine;
385420
for (int k = 0; k < frame.Components.Length; k++)
@@ -420,6 +455,17 @@ ref Unsafe.Add(ref blockRef, blockCol),
420455
{
421456
this.FlushToStream();
422457
}
458+
459+
if (this.restartInterval > 0)
460+
{
461+
if (restartsToGo == 0)
462+
{
463+
restartsToGo = this.restartInterval;
464+
restarts++;
465+
}
466+
467+
restartsToGo--;
468+
}
423469
}
424470
}
425471

@@ -554,7 +600,7 @@ private void WriteBlock(
554600
}
555601

556602
private void WriteRestart(int restart) =>
557-
this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)]);
603+
this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
558604

559605
/// <summary>
560606
/// Emits the most significant count of bits to the buffer.

src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
100100
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
101101

102102
// Write the Huffman tables.
103-
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
103+
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
104104
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
105105

106106
// Write the quantization tables.
@@ -445,7 +445,7 @@ private void WriteDri(int restartInterval, Span<byte> buffer)
445445

446446
buffer[1] = (byte)(restartInterval & 0xff);
447447
buffer[0] = (byte)(restartInterval >> 8);
448-
this.outputStream.Write(buffer);
448+
this.outputStream.Write(buffer, 0, 2);
449449
}
450450

451451
/// <summary>
@@ -764,7 +764,7 @@ private void WriteProgressiveScans<TPixel>(
764764
{
765765
this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
766766

767-
encoder.EncodeDcScan(frame.Components[i], this.encoder.RestartInterval, cancellationToken);
767+
encoder.EncodeDcScan(frame.Components[i], cancellationToken);
768768
}
769769

770770
// Phase 2: AC scans
@@ -779,7 +779,7 @@ private void WriteProgressiveScans<TPixel>(
779779
{
780780
this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
781781

782-
encoder.EncodeAcScan(frame.Components[i], start, end, this.encoder.RestartInterval, cancellationToken);
782+
encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
783783
}
784784
}
785785
}

tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,39 @@ public void EncodeProgressive_DefaultNumberOfScans<TPixel>(TestImageProvider<TPi
178178
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
179179
}
180180

181+
[Theory]
182+
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
183+
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
184+
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
185+
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
186+
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
187+
public void EncodeProgressive_CustomNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
188+
where TPixel : unmanaged, IPixel<TPixel>
189+
{
190+
using Image<TPixel> image = provider.GetImage();
191+
192+
JpegEncoder encoder = new()
193+
{
194+
Quality = quality,
195+
ColorType = colorType,
196+
Progressive = true,
197+
ProgressiveScans = 4,
198+
RestartInterval = 7
199+
};
200+
string info = $"{colorType}-Q{quality}";
201+
202+
using MemoryStream ms = new();
203+
image.SaveAsJpeg(ms, encoder);
204+
ms.Position = 0;
205+
206+
// TEMP: Save decoded output as PNG so we can do a pixel compare.
207+
using Image<TPixel> image2 = Image.Load<TPixel>(ms);
208+
image2.DebugSave(provider, testOutputDetails: info, extension: "png");
209+
210+
ImageComparer comparer = new TolerantImageComparer(tolerance);
211+
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
212+
}
213+
181214
[Theory]
182215
[InlineData(JpegEncodingColor.YCbCrRatio420)]
183216
[InlineData(JpegEncodingColor.YCbCrRatio444)]

0 commit comments

Comments
 (0)