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 ac527ff312..90e16f6dff 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,133 @@ ref Unsafe.Add(ref blockRef, k), { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + 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]; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + 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++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + component.DcPredictor = 0; + } + + this.WriteDc( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } + } + } + + 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; + + int restarts = 0; + int restartsToGo = this.restartInterval; + + 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++) + { + if (this.restartInterval > 0 && restartsToGo == 0) + { + this.FlushRemainingBytes(); + this.WriteRestart(restarts % 8); + } + + this.WriteAcBlock( + ref Unsafe.Add(ref blockRef, k), + start, + end, + ref acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -250,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(); @@ -260,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++) @@ -300,6 +455,17 @@ ref Unsafe.Add(ref blockRef, blockCol), { this.FlushToStream(); } + + if (this.restartInterval > 0) + { + if (restartsToGo == 0) + { + restartsToGo = this.restartInterval; + restarts++; + } + + restartsToGo--; + } } } @@ -371,25 +537,29 @@ 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; - nint lastValuableIndex = block.GetLastNonZeroIndex(); - int runLength = 0; ref short blockRef = ref Unsafe.As(ref block); - for (nint zig = 1; zig <= lastValuableIndex; zig++) + for (nint zig = start; zig < end; zig++) { const int zeroRun1 = 1 << 4; const int zeroRun16 = 16 << 4; @@ -413,14 +583,25 @@ private void WriteBlock( } // 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) + 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); + this.WriteAcBlock(ref block, 1, 64, ref acTable); + } + + private void WriteRestart(int 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/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 0daaae112c..69f04f1dcf 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder /// private int? quality; + /// + /// Backing field for + /// + private int progressiveScans = 4; + + /// + /// Backing field for + /// + private int restartInterval; + /// /// 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 +43,56 @@ 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 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 a6ff623660..34028c2f83 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -100,12 +100,15 @@ 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. 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, 0, 2); + } + /// /// Writes the App1 header. /// @@ -563,7 +585,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); @@ -597,7 +620,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", @@ -630,8 +663,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); } @@ -666,7 +699,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); @@ -694,6 +734,50 @@ private void WriteHuffmanScans( } } + /// + /// Writes the progressive scans + /// + /// The type of pixel format. + /// The current frame. + /// The frame configuration. + /// The scan encoder. + /// Temporary buffer. + /// The cancellation token. + private void WriteProgressiveScans( + JpegFrame frame, + JpegFrameConfig frameConfig, + HuffmanScanEncoder encoder, + Span buffer, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + 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. /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 1f4b3e4656..58b437af0f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -160,6 +160,64 @@ 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, JpegColorType 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] + [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, JpegColorType 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(JpegColorType.YCbCrRatio420)] [InlineData(JpegColorType.YCbCrRatio444)]