From 368f89e4509a053a35c5b52d9fc679ba6163c10a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 21 May 2021 16:11:17 +0300 Subject: [PATCH] Moved quantization table initialization logic to JpegEncoderCore --- .../Encoder/YCbCrEncoder{TPixel}.cs | 146 +++--------------- .../Formats/Jpeg/JpegEncoderCore.cs | 110 ++++++++++++- 2 files changed, 123 insertions(+), 133 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs index 051acf0e84..db2a3c354f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs @@ -41,16 +41,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// private uint bitCount; - /// - /// The scaled chrominance table, in zig-zag order. - /// - private Block8x8F chrominanceQuantTable; - - /// - /// The scaled luminance table, in zig-zag order. - /// - private Block8x8F luminanceQuantTable; - private Block8x8F temporalBlock1; private Block8x8F temporalBlock2; @@ -82,71 +72,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder 8, 8, 8, }; - /// - /// Gets the unscaled quantization tables in zig-zag order. Each - /// encoder copies and scales the tables according to its quality parameter. - /// The values are derived from section K.1 after converting from natural to - /// zig-zag order. - /// - // The C# compiler emits this as a compile-time constant embedded in the PE file. - // This is effectively compiled down to: return new ReadOnlySpan(&data, length) - // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] - { - // Luminance. - 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, - 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, - 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, - 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, - 100, 120, 92, 101, 103, 99, - }; - - /// - /// Gets the unscaled quantization tables in zig-zag order. Each - /// encoder copies and scales the tables according to its quality parameter. - /// The values are derived from section K.1 after converting from natural to - /// zig-zag order. - /// - // The C# compiler emits this as a compile-time constant embedded in the PE file. - // This is effectively compiled down to: return new ReadOnlySpan(&data, length) - // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] - { - // Chrominance. - 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - }; - - - public ref Block8x8F ChrominanceQuantizationTable => ref this.chrominanceQuantTable; - - public ref Block8x8F LuminanceQuantizationTable => ref this.luminanceQuantTable; - - - public YCbCrEncoder(Stream outputStream, int componentCount, int quality) + public YCbCrEncoder(Stream outputStream) { this.target = outputStream; - - // Convert from a quality rating to a scaling factor. - int scale; - if (quality < 50) - { - scale = 5000 / quality; - } - else - { - scale = 200 - (quality * 2); - } - - // Initialize the quantization tables. - InitQuantizationTable(0, scale, ref this.luminanceQuantTable); - if (componentCount > 1) - { - InitQuantizationTable(1, scale, ref this.chrominanceQuantTable); - } } /// @@ -155,12 +83,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// The pixel format. /// The pixel accessor providing access to the image pixels. /// The token to monitor for cancellation. - private void Encode444(Image pixels, CancellationToken cancellationToken) + private void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; - Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; - var unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming @@ -184,21 +109,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder QuantIndex.Luminance, prevDCY, ref pixelConverter.Y, - ref onStackLuminanceQuantTable, + ref luminanceQuantTable, ref unzig); prevDCCb = this.WriteBlock( QuantIndex.Chrominance, prevDCCb, ref pixelConverter.Cb, - ref onStackChrominanceQuantTable, + ref chrominanceQuantTable, ref unzig); prevDCCr = this.WriteBlock( QuantIndex.Chrominance, prevDCCr, ref pixelConverter.Cr, - ref onStackChrominanceQuantTable, + ref chrominanceQuantTable, ref unzig); } } @@ -211,7 +136,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// The pixel format. /// The pixel accessor providing access to the image pixels. /// The token to monitor for cancellation. - private void Encode420(Image pixels, CancellationToken cancellationToken) + private void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -219,9 +144,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder Span cb = stackalloc Block8x8F[4]; Span cr = stackalloc Block8x8F[4]; - Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; - Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; - var unzig = ZigZag.CreateUnzigTable(); var pixelConverter = YCbCrForwardConverter.Create(); @@ -252,7 +174,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder QuantIndex.Luminance, prevDCY, ref pixelConverter.Y, - ref onStackLuminanceQuantTable, + ref luminanceQuantTable, ref unzig); } @@ -261,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder QuantIndex.Chrominance, prevDCCb, ref b, - ref onStackChrominanceQuantTable, + ref chrominanceQuantTable, ref unzig); Block8x8F.Scale16X16To8X8(ref b, cr); @@ -269,7 +191,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder QuantIndex.Chrominance, prevDCCr, ref b, - ref onStackChrominanceQuantTable, + ref chrominanceQuantTable, ref unzig); } } @@ -282,11 +204,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// The pixel format. /// The pixel accessor providing access to the image pixels. /// The token to monitor for cancellation. - private void EncodeGrayscale(Image pixels, CancellationToken cancellationToken) + private void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; - var unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming @@ -310,28 +230,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder QuantIndex.Luminance, prevDCY, ref pixelConverter.Y, - ref onStackLuminanceQuantTable, + ref luminanceQuantTable, ref unzig); } } } - public void WriteStartOfScan(Image image, JpegColorType? colorType, JpegSubsample? subsample, CancellationToken cancellationToken) + public void WriteStartOfScan( + Image image, + JpegColorType? colorType, + JpegSubsample? subsample, + ref Block8x8F luminanceQuantTable, + ref Block8x8F chrominanceTable, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (colorType == JpegColorType.Luminance) { - this.EncodeGrayscale(image, cancellationToken); + this.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); } else { switch (subsample) { case JpegSubsample.Ratio444: - this.Encode444(image, cancellationToken); + this.Encode444(image, ref luminanceQuantTable, ref chrominanceTable, cancellationToken); break; case JpegSubsample.Ratio420: - this.Encode420(image, cancellationToken); + this.Encode420(image, ref luminanceQuantTable, ref chrominanceTable, cancellationToken); break; } } @@ -499,35 +425,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.Emit((uint)b & (uint)((1 << ((int)bt)) - 1), bt); } } - - - /// - /// Initializes quantization table. - /// - /// The quantization index. - /// The scaling factor. - /// The quantization table. - private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) - { - DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); - ReadOnlySpan unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance; - - for (int j = 0; j < Block8x8F.Size; j++) - { - int x = unscaledQuant[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - quant[j] = x; - } - } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 2625d490c0..6b58ef483c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -31,6 +31,45 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private const int QuantizationTableCount = 2; + /// + /// Gets the unscaled quantization tables in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + { + // Luminance. + 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, + 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, + 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, + 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, + 100, 120, 92, 101, 103, 99, + }; + + /// + /// Gets the unscaled quantization tables in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + { + // Chrominance. + 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + }; + + /// /// A scratch buffer to reduce allocations. /// @@ -97,7 +136,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - YCbCrEncoder scanEncoder = new YCbCrEncoder(stream, componentCount, qlty); + // Convert from a quality rating to a scaling factor. + int scale; + if (qlty < 50) + { + scale = 5000 / qlty; + } + else + { + scale = 200 - (qlty * 2); + } + + // Initialize the quantization tables. + // TODO: This looks ugly, should we write chrominance table for luminance-only images? + // If not - this can code can be simplified + Block8x8F luminanceQuantTable = default; + Block8x8F chrominanceQuantTable = default; + InitQuantizationTable(0, scale, ref luminanceQuantTable); + if (componentCount > 1) + { + InitQuantizationTable(1, scale, ref chrominanceQuantTable); + } // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -106,7 +165,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteProfiles(metadata); // Write the quantization tables. - this.WriteDefineQuantizationTables(ref scanEncoder.LuminanceQuantizationTable, ref scanEncoder.ChrominanceQuantizationTable); + this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); // Write the image dimensions. this.WriteStartOfFrame(image.Width, image.Height, componentCount); @@ -114,8 +173,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Write the Huffman tables. this.WriteDefineHuffmanTables(componentCount); - // Write the image data. - this.WriteStartOfScan(scanEncoder, image, componentCount, cancellationToken); + // Write the scan header. + this.WriteStartOfScan(image, componentCount, cancellationToken); + + // Write the scan compressed data. + new YCbCrEncoder(stream).WriteStartOfScan( + image, + this.colorType, + this.subsample, + ref luminanceQuantTable, + ref chrominanceQuantTable, + cancellationToken); // Write the End Of Image marker. this.buffer[0] = JpegConstants.Markers.XFF; @@ -573,7 +641,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The pixel accessor providing access to the image pixels. /// The number of components in a pixel. /// The token to monitor for cancellation. - private void WriteStartOfScan(YCbCrEncoder scanEncoder, Image image, int componentCount, CancellationToken cancellationToken) + private void WriteStartOfScan(Image image, int componentCount, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -618,9 +686,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[sosSize] = 0x3f; // Se - End of spectral selection. this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) this.outputStream.Write(this.buffer, 0, sosSize + 2); - - - scanEncoder.WriteStartOfScan(image, this.colorType, this.subsample, cancellationToken); } /// @@ -637,5 +702,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[3] = (byte)(length & 0xff); this.outputStream.Write(this.buffer, 0, 4); } + + /// + /// Initializes quantization table. + /// + /// The quantization index. + /// The scaling factor. + /// The quantization table. + private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) + { + DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); + ReadOnlySpan unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance; + + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = unscaledQuant[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + quant[j] = x; + } + } } }