From a059f4e9bdbc00c24b1801688303072af6a6f2a8 Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Tue, 21 May 2024 19:48:08 +0300 Subject: [PATCH 1/7] Add progressive JPEG encoder --- .../Components/Encoder/HuffmanScanEncoder.cs | 130 +++++++++++++++++- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 33 +++++ .../Formats/Jpeg/JpegEncoderCore.cs | 77 ++++++++++- 3 files changed, 232 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index d74494f9e5..991f7ff326 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -237,6 +237,82 @@ ref Unsafe.Add(ref blockRef, k), this.FlushRemainingBytes(); } + /// + /// Encodes the DC coefficients for a given component's blocks in a scan. + /// + /// The component whose DC coefficients need to be encoded. + /// The token to request cancellation. + 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 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(); + } + + /// + /// Encodes the AC coefficients for a specified range of blocks in a component's scan. + /// + /// 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. + /// The token to request cancellation. + 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 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(); + } + /// /// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors. /// @@ -371,16 +447,64 @@ 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; + + int runLength = 0; + ref short blockRef = ref Unsafe.As(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; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 5ff4b1694d..86921e73db 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -13,6 +13,11 @@ public sealed class JpegEncoder : ImageEncoder /// private int? quality; + /// + /// Backing field for + /// + private int progressiveScans = 4; + /// /// 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 int? Quality } } + /// + /// Gets a value indicating whether progressive encoding is used. + /// + public bool Progressive { get; init; } + + /// + /// Gets number of scans per component for progressive encoding. + /// Defaults to 4. + /// + /// + /// 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. + /// + /// Progressive scans must be in [2..64] range. + 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; + } + } + /// /// Gets the component encoding mode. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 243bbe051d..19e5e88ca6 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -108,7 +108,14 @@ public void Encode(Image image, Stream stream, CancellationToken // Write scans with actual pixel data using SpectralConverter 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 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa // 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 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Spa /// /// The collecction of component configuration items. /// Temporary buffer. - private void WriteStartOfScan(Span components, Span buffer) + private void WriteStartOfScan(Span components, Span buffer) => + this.WriteStartOfScan(components, buffer, 0x00, 0x3f); + + /// + /// Writes the StartOfScan marker. + /// + /// The collecction of component configuration items. + /// Temporary buffer. + /// Start of spectral selection + /// End of spectral selection + private void WriteStartOfScan(Span components, Span 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 @@ private void WriteStartOfScan(Span components, Span b 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 @@ private void WriteHuffmanScans( } } + /// + /// Writes the progressive scans + /// + /// The type of pixel format. + /// The current frame. + /// The frame configuration. + /// The spectral converter. + /// The scan encoder. + /// Temporary buffer. + /// The cancellation token. + private void WriteProgressiveScans( + JpegFrame frame, + JpegFrameConfig frameConfig, + SpectralConverter spectralConverter, + HuffmanScanEncoder encoder, + Span buffer, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + frame.AllocateComponents(fullScan: true); + spectralConverter.ConvertFull(); + + Span 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); + } + } + } + /// /// Writes the header for a marker with the given length. /// From 490c0d3a314edc2c91c720b5ff204ddc25628dd1 Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Wed, 22 May 2024 23:31:26 +0300 Subject: [PATCH 2/7] update write block in huffman encoder, add test --- .../Components/Encoder/HuffmanScanEncoder.cs | 41 +------------------ .../Formats/Jpg/JpegEncoderTests.cs | 32 +++++++++++---- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 991f7ff326..014a06c16a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -263,7 +263,6 @@ public void EncodeDcScan(Component component, CancellationToken cancellationToke ref Unsafe.Add(ref blockRef, k), ref dcHuffmanTable); - if (this.IsStreamFlushNeeded) { this.FlushToStream(); @@ -492,6 +491,7 @@ private void WriteAcBlock( } } + // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over if (runLength > 0) { this.EmitHuff(acHuffTable, 0x00); @@ -505,44 +505,7 @@ private void WriteBlock( ref HuffmanLut acTable) { this.WriteDc(component, ref block, ref dcTable); - - // Emit the AC components. - int[] acHuffTable = acTable.Values; - - nint lastValuableIndex = block.GetLastNonZeroIndex(); - - int runLength = 0; - ref short blockRef = ref Unsafe.As(ref block); - for (nint zig = 1; zig <= lastValuableIndex; 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 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) - { - this.EmitHuff(acHuffTable, 0x00); - } + this.WriteAcBlock(ref block, 1, 64, ref acTable); } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 5842c8e1a0..ee3cc674d3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -14,13 +14,6 @@ public partial class JpegEncoderTests { private static JpegEncoder JpegEncoder => new(); - private static readonly TheoryData TestQualities = new() - { - 40, - 80, - 100, - }; - public static readonly TheoryData NonSubsampledEncodingSetups = new() { { JpegEncodingColor.Rgb, 100, 0.0238f / 100 }, @@ -160,6 +153,31 @@ public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvid TestJpegEncoderCore(provider, colorType, 100, comparer); } + [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_DefaultNumberOfScans(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 + }; + string info = $"{colorType}-Q{quality}"; + + ImageComparer comparer = new TolerantImageComparer(tolerance); + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg"); + } + [Theory] [InlineData(JpegEncodingColor.YCbCrRatio420)] [InlineData(JpegEncodingColor.YCbCrRatio444)] From dc5affac08b369e0318d4269f6701926f83a46d1 Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Wed, 22 May 2024 23:43:09 +0300 Subject: [PATCH 3/7] update jpeg encoder core --- .../Formats/Jpeg/JpegEncoderCore.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 19e5e88ca6..bc85194096 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -108,14 +108,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Write scans with actual pixel data using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables); - if (this.encoder.Progressive) - { - this.WriteProgressiveScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); - } - else - { - this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); - } + this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); // Write the End Of Image marker. this.WriteEndOfImageMarker(buffer); @@ -690,7 +683,14 @@ private void WriteHuffmanScans( CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - if (frame.Components.Length == 1) + if (this.encoder.Progressive) + { + frame.AllocateComponents(fullScan: true); + spectralConverter.ConvertFull(); + + this.WriteProgressiveScans(frame, frameConfig, encoder, buffer, cancellationToken); + } + else if (frame.Components.Length == 1) { frame.AllocateComponents(fullScan: false); @@ -724,22 +724,17 @@ private void WriteHuffmanScans( /// The type of pixel format. /// The current frame. /// The frame configuration. - /// The spectral converter. /// The scan encoder. /// Temporary buffer. /// The cancellation token. private void WriteProgressiveScans( JpegFrame frame, JpegFrameConfig frameConfig, - SpectralConverter spectralConverter, HuffmanScanEncoder encoder, Span buffer, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - frame.AllocateComponents(fullScan: true); - spectralConverter.ConvertFull(); - Span components = frameConfig.Components; // Phase 1: DC scan From f131d60a6b65663e44decf7ab7fe48e0f710907f Mon Sep 17 00:00:00 2001 From: ardabada Date: Mon, 8 Jul 2024 11:38:57 +0300 Subject: [PATCH 4/7] Add restart interval --- .../Components/Encoder/HuffmanScanEncoder.cs | 52 ++++++++++++++++++- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 27 ++++++++++ .../Formats/Jpeg/JpegEncoderCore.cs | 26 +++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 014a06c16a..cd7eb64315 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -241,14 +241,18 @@ ref Unsafe.Add(ref blockRef, k), /// 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, CancellationToken cancellationToken) + 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(); @@ -258,6 +262,13 @@ public void EncodeDcScan(Component component, CancellationToken cancellationToke 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), @@ -267,6 +278,18 @@ ref Unsafe.Add(ref blockRef, k), { this.FlushToStream(); } + + if (restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = restartInterval; + restarts++; + restarts &= 7; + } + + restartsToGo--; + } } } @@ -279,12 +302,16 @@ ref Unsafe.Add(ref blockRef, k), /// 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, CancellationToken cancellationToken) + 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++) @@ -296,6 +323,12 @@ public void EncodeAcScan(Component component, nint start, nint end, Cancellation 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, @@ -306,6 +339,18 @@ ref Unsafe.Add(ref blockRef, k), { this.FlushToStream(); } + + if (restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = restartInterval; + restarts++; + restarts &= 7; + } + + restartsToGo--; + } } } @@ -508,6 +553,9 @@ private void WriteBlock( this.WriteAcBlock(ref block, 1, 64, ref acTable); } + private void WriteRestart(int restart) => + this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)]); + /// /// Emits the most significant count of bits to the buffer. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 86921e73db..01e6624670 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -18,6 +18,11 @@ public sealed class JpegEncoder : ImageEncoder /// private int progressiveScans = 4; + /// + /// Backing field for + /// + private int restartInterval = 0; + /// /// Gets the quality, that will be used to encode the image. Quality /// index must be between 1 and 100 (compression from max to min). @@ -66,6 +71,28 @@ public int ProgressiveScans } } + /// + /// Gets numbers of MCUs between restart markers. + /// Defaults to 0. + /// + /// + /// Currently supported in progressive encoding only. + /// + /// Restart interval must be in [0..65535] range. + 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; + } + } + /// /// Gets the component encoding mode. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index bc85194096..3c1d28b420 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -106,6 +106,9 @@ public void Encode(Image image, Stream stream, CancellationToken // Write the quantization tables. this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); + // Write define restart interval + this.WriteDri(this.encoder.RestartInterval, buffer); + // Write scans with actual pixel data using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables); this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); @@ -426,6 +429,25 @@ private void WriteXmpProfile(XmpProfile xmpProfile, Span buffer) } } + /// + /// Writes the DRI marker + /// + /// Numbers of MCUs between restart markers. + /// Temporary buffer. + private void WriteDri(int restartInterval, Span buffer) + { + if (restartInterval <= 0) + { + return; + } + + this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer); + + buffer[1] = (byte)(restartInterval & 0xff); + buffer[0] = (byte)(restartInterval >> 8); + this.outputStream.Write(buffer); + } + /// /// Writes the App1 header. /// @@ -742,7 +764,7 @@ private void WriteProgressiveScans( { this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00); - encoder.EncodeDcScan(frame.Components[i], cancellationToken); + encoder.EncodeDcScan(frame.Components[i], this.encoder.RestartInterval, cancellationToken); } // Phase 2: AC scans @@ -757,7 +779,7 @@ private void WriteProgressiveScans( { this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1)); - encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken); + encoder.EncodeAcScan(frame.Components[i], start, end, this.encoder.RestartInterval, cancellationToken); } } } From 3f815722dfd9156d37bb27406973747222eb3fcf Mon Sep 17 00:00:00 2001 From: ardabada Date: Thu, 11 Jul 2024 10:06:35 +0300 Subject: [PATCH 5/7] fix build (maybe) --- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 01e6624670..9673ad635c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -21,7 +21,7 @@ public sealed class JpegEncoder : ImageEncoder /// /// Backing field for /// - private int restartInterval = 0; + private int restartInterval; /// /// Gets the quality, that will be used to encode the image. Quality From a34bca3b660a57a1b3c15aadf4c4bf9cfe0591d9 Mon Sep 17 00:00:00 2001 From: ardabada Date: Tue, 8 Oct 2024 18:15:15 +0300 Subject: [PATCH 6/7] Update encoder and decoder to handle restart interval --- .../Components/Decoder/HuffmanScanDecoder.cs | 2 + .../Components/Encoder/HuffmanScanEncoder.cs | 78 +++++++++++++++---- .../Formats/Jpeg/JpegEncoderCore.cs | 8 +- .../Formats/Jpg/JpegEncoderTests.cs | 33 ++++++++ 4 files changed, 101 insertions(+), 20 deletions(-) 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 @@ public void ParseEntropyCodedData(int scanComponentCount) 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 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati 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 @@ public void EncodeScanBaseline(Component component, CancellationToken cancellati 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 @@ ref Unsafe.Add(ref blockRef, k), { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -241,9 +267,8 @@ ref Unsafe.Add(ref blockRef, k), /// 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 @@ public void EncodeDcScan(Component component, int restartInterval, CancellationT 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 @@ public void EncodeDcScan(Component component, int restartInterval, CancellationT 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 @@ ref Unsafe.Add(ref blockRef, k), 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 @@ ref Unsafe.Add(ref blockRef, k), /// 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 @@ public void EncodeAcScan(Component component, nint start, nint end, int restartI 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 @@ ref Unsafe.Add(ref blockRef, k), 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 @@ private void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConv 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 @@ private void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConv // 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 @@ ref Unsafe.Add(ref blockRef, blockCol), { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -554,7 +600,7 @@ private void WriteBlock( } 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ private void WriteDri(int restartInterval, Span buffer) buffer[1] = (byte)(restartInterval & 0xff); buffer[0] = (byte)(restartInterval >> 8); - this.outputStream.Write(buffer); + this.outputStream.Write(buffer, 0, 2); } /// @@ -764,7 +764,7 @@ private void WriteProgressiveScans( { 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 @@ private void WriteProgressiveScans( { 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 void EncodeProgressive_DefaultNumberOfScans(TestImageProvider(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)] From 4ff51b9a0399af608a661980b10a7bf32e45cb37 Mon Sep 17 00:00:00 2001 From: ardabada Date: Tue, 8 Oct 2024 18:26:19 +0300 Subject: [PATCH 7/7] fix --- .../ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index b5f17d0b26..58b437af0f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -14,7 +14,6 @@ public partial class JpegEncoderTests { private static JpegEncoder JpegEncoder => new(); - public static readonly TheoryData NonSubsampledEncodingSetups = new() private static readonly TheoryData TestQualities = new() { 40, @@ -167,7 +166,7 @@ public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvid [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_DefaultNumberOfScans(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeProgressive_DefaultNumberOfScans(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -192,7 +191,7 @@ public void EncodeProgressive_DefaultNumberOfScans(TestImageProvider(TestImageProvider provider, JpegEncodingColor colorType, int quality, float tolerance) + public void EncodeProgressive_CustomNumberOfScans(TestImageProvider provider, JpegColorType colorType, int quality, float tolerance) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -220,9 +219,9 @@ public void EncodeProgressive_CustomNumberOfScans(TestImageProvider