diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index bbd2bff53b..56e0f1e985 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -119,6 +119,8 @@ internal class HuffmanScanDecoder : IJpegScanDecoder this.frame.AllocateComponents(); + this.todo = this.restartInterval; + if (!this.frame.Progressive) { this.ParseBaselineData(); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index cd7eb64315..000a463097 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -87,6 +87,8 @@ internal class HuffmanScanEncoder /// private readonly byte[] streamWriteBuffer; + private readonly int restartInterval; + /// /// Number of jagged bits stored in /// @@ -103,13 +105,16 @@ internal class HuffmanScanEncoder /// Initializes a new instance of the class. /// /// Amount of encoded 8x8 blocks per single jpeg macroblock. + /// Numbers of MCUs between restart markers. /// Output stream for saving encoded data. - 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; @@ -211,6 +216,9 @@ internal class HuffmanScanEncoder 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(); @@ -221,6 +229,13 @@ internal class HuffmanScanEncoder 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), @@ -231,6 +246,17 @@ internal class HuffmanScanEncoder { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -241,9 +267,8 @@ internal class HuffmanScanEncoder /// Encodes the DC coefficients for a given component's blocks in a scan. /// /// The component whose DC coefficients need to be encoded. - /// Numbers of MCUs between restart markers. /// The token to request cancellation. - public void EncodeDcScan(Component component, int restartInterval, CancellationToken cancellationToken) + public void EncodeDcScan(Component component, CancellationToken cancellationToken) { int h = component.HeightInBlocks; int w = component.WidthInBlocks; @@ -251,7 +276,7 @@ internal class HuffmanScanEncoder ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; int restarts = 0; - int restartsToGo = restartInterval; + int restartsToGo = this.restartInterval; for (int i = 0; i < h; i++) { @@ -262,7 +287,7 @@ internal class HuffmanScanEncoder for (nuint k = 0; k < (uint)w; k++) { - if (restartInterval > 0 && restartsToGo == 0) + if (this.restartInterval > 0 && restartsToGo == 0) { this.FlushRemainingBytes(); this.WriteRestart(restarts % 8); @@ -279,13 +304,12 @@ internal class HuffmanScanEncoder this.FlushToStream(); } - if (restartInterval > 0) + if (this.restartInterval > 0) { if (restartsToGo == 0) { - restartsToGo = restartInterval; + restartsToGo = this.restartInterval; restarts++; - restarts &= 7; } restartsToGo--; @@ -302,15 +326,14 @@ internal class HuffmanScanEncoder /// The component whose AC coefficients need to be encoded. /// The starting index of the AC coefficient range to encode. /// The ending index of the AC coefficient range to encode. - /// Numbers of MCUs between restart markers. /// The token to request cancellation. - public void EncodeAcScan(Component component, nint start, nint end, int restartInterval, CancellationToken cancellationToken) + public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken) { int h = component.HeightInBlocks; int w = component.WidthInBlocks; int restarts = 0; - int restartsToGo = restartInterval; + int restartsToGo = this.restartInterval; ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; @@ -323,7 +346,7 @@ internal class HuffmanScanEncoder for (nuint k = 0; k < (uint)w; k++) { - if (restartInterval > 0 && restartsToGo == 0) + if (this.restartInterval > 0 && restartsToGo == 0) { this.FlushRemainingBytes(); this.WriteRestart(restarts % 8); @@ -340,13 +363,12 @@ internal class HuffmanScanEncoder this.FlushToStream(); } - if (restartInterval > 0) + if (this.restartInterval > 0) { if (restartsToGo == 0) { - restartsToGo = restartInterval; + restartsToGo = this.restartInterval; restarts++; - restarts &= 7; } restartsToGo--; @@ -370,6 +392,9 @@ internal class HuffmanScanEncoder int mcusPerColumn = frame.McusPerColumn; int mcusPerLine = frame.McusPerLine; + int restarts = 0; + int restartsToGo = this.restartInterval; + for (int j = 0; j < mcusPerColumn; j++) { cancellationToken.ThrowIfCancellationRequested(); @@ -380,6 +405,16 @@ internal class HuffmanScanEncoder // 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++) @@ -420,6 +455,17 @@ internal class HuffmanScanEncoder { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -554,7 +600,7 @@ internal class HuffmanScanEncoder } private void WriteRestart(int restart) => - this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)]); + this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2); /// /// Emits the most significant count of bits to the buffer. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 3c1d28b420..11860225ba 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -100,7 +100,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); // Write the Huffman tables. - HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); + HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); // Write the quantization tables. @@ -445,7 +445,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals buffer[1] = (byte)(restartInterval & 0xff); buffer[0] = (byte)(restartInterval >> 8); - this.outputStream.Write(buffer); + this.outputStream.Write(buffer, 0, 2); } /// @@ -764,7 +764,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals { this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00); - encoder.EncodeDcScan(frame.Components[i], this.encoder.RestartInterval, cancellationToken); + encoder.EncodeDcScan(frame.Components[i], cancellationToken); } // Phase 2: AC scans @@ -779,7 +779,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals { this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1)); - encoder.EncodeAcScan(frame.Components[i], start, end, this.encoder.RestartInterval, cancellationToken); + encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index ee3cc674d3..bce10ec3d5 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -178,6 +178,39 @@ public partial class JpegEncoderTests image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg"); } + [Theory] + [WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)] + [WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)] + [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)] + [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)] + public void EncodeProgressive_CustomNumberOfScans(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) +where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + JpegEncoder encoder = new() + { + Quality = quality, + ColorType = colorType, + Progressive = true, + ProgressiveScans = 4, + RestartInterval = 7 + }; + string info = $"{colorType}-Q{quality}"; + + using MemoryStream ms = new(); + image.SaveAsJpeg(ms, encoder); + ms.Position = 0; + + // TEMP: Save decoded output as PNG so we can do a pixel compare. + using Image image2 = Image.Load(ms); + image2.DebugSave(provider, testOutputDetails: info, extension: "png"); + + ImageComparer comparer = new TolerantImageComparer(tolerance); + image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg"); + } + [Theory] [InlineData(JpegEncodingColor.YCbCrRatio420)] [InlineData(JpegEncodingColor.YCbCrRatio444)]