From ea81abc4f0f3c625db137bc6dd22c228c1d2cd8c Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 9 May 2022 03:23:46 +0300 Subject: [PATCH] Implemented quantization tables --- .../Jpeg/Components/Block8x8.Intrinsic.cs | 2 +- .../Formats/Jpeg/Components/Block8x8.cs | 17 +++- .../Jpeg/Components/Block8x8F.Generated.cs | 2 +- .../Jpeg/Components/Block8x8F.Intrinsic.cs | 2 +- .../Jpeg/Components/Block8x8F.ScaledCopyTo.cs | 2 +- .../Formats/Jpeg/Components/Block8x8F.cs | 2 +- .../Components/Encoder/HuffmanScanEncoder.cs | 13 ++- .../Formats/Jpeg/Components/Quantization.cs | 14 +++- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 12 ++- .../Formats/Jpeg/JpegEncoderCore.cs | 84 ++++++++++--------- 10 files changed, 93 insertions(+), 57 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs index 002d382dc6..0a6c099d02 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs @@ -7,7 +7,7 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal unsafe partial struct Block8x8 + public unsafe partial struct Block8x8 { [FieldOffset(0)] public Vector128 V0; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs index 4b03f9f7b9..3bcfbdfa59 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// // ReSharper disable once InconsistentNaming [StructLayout(LayoutKind.Explicit)] - internal unsafe partial struct Block8x8 + public unsafe partial struct Block8x8 { /// /// A number of scalar coefficients in a @@ -122,6 +122,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } } + public static Block8x8 Load(ReadOnlySpan data) + { + Unsafe.SkipInit(out Block8x8 result); + result.LoadFrom(data); + return result; + } + + public void LoadFrom(ReadOnlySpan source) + { + for (int i = 0; i < Size; i++) + { + this[i] = source[i]; + } + } + /// /// Load raw 16bit integers from source. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs index dd5d3f1960..8b40ff5c6f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs @@ -8,7 +8,7 @@ using System.Runtime.CompilerServices; // namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal partial struct Block8x8F + public partial struct Block8x8F { /// /// Level shift by +maximum/2, clip to [0, maximum] diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs index 0971ccdca0..dc86442624 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Intrinsic.cs @@ -11,7 +11,7 @@ using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal partial struct Block8x8F + public partial struct Block8x8F { /// /// A number of rows of 8 scalar coefficients each in diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs index ae2d1f722c..fa9aa24498 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Memory; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal partial struct Block8x8F + public partial struct Block8x8F { /// /// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical scale factors. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 0190fc7454..3e8fddebf7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// 8x8 matrix of coefficients. /// [StructLayout(LayoutKind.Explicit)] - internal partial struct Block8x8F : IEquatable + public partial struct Block8x8F : IEquatable { /// /// A number of scalar coefficients in a diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 6b69e43209..3d61967ef3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -62,12 +62,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// The DC Huffman tables. /// - private readonly HuffmanLut[] dcHuffmanTables; + private readonly HuffmanLut[] dcHuffmanTables = new HuffmanLut[4]; /// /// The AC Huffman tables. /// - private readonly HuffmanLut[] acHuffmanTables; + private readonly HuffmanLut[] acHuffmanTables = new HuffmanLut[4]; /// /// Emitted bits 'micro buffer' before being transferred to the . @@ -119,9 +119,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier]; this.target = outputStream; - - this.dcHuffmanTables = new HuffmanLut[4]; - this.acHuffmanTables = new HuffmanLut[4]; } /// @@ -135,10 +132,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder get => this.emitWriteIndex < (uint)this.emitBuffer.Length / 2; } - public void BuildHuffmanTable(JpegHuffmanTableConfig table) + public void BuildHuffmanTable(JpegHuffmanTableConfig tableConfig) { - HuffmanLut[] tables = table.Class == 0 ? this.dcHuffmanTables : this.acHuffmanTables; - tables[table.DestinationIndex] = new HuffmanLut(table.TableSpec); + HuffmanLut[] tables = tableConfig.Class == 0 ? this.dcHuffmanTables : this.acHuffmanTables; + tables[tableConfig.DestinationIndex] = new HuffmanLut(tableConfig.Table); } public void EncodeInterleavedScan(JpegFrame frame, Image image, Block8x8F[] quantTables, Configuration configuration, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index eab5e6a082..b85503ee15 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -176,7 +176,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components return quality < 50 ? (5000 / quality) : (200 - (quality * 2)); } - private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + public static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + { + Block8x8F table = default; + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = ((unscaledTable[j] * scale) + 50) / 100; + table[j] = Numerics.Clamp(x, 1, 255); + } + + return table; + } + + public static Block8x8F ScaleQuantizationTable(int scale, Block8x8 unscaledTable) { Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index d92fc457a6..371f543cf9 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; using SixLabors.ImageSharp.PixelFormats; @@ -125,11 +126,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public int DestinationIndex { get; set; } - public HuffmanSpec TableSpec { get; set; } + public HuffmanSpec Table { get; set; } + } + + public class JpegQuantizationTableConfig + { + public int DestinationIndex { get; set; } + + public Block8x8 Table { get; set; } } public class JpegScanConfig { public JpegHuffmanTableConfig[] HuffmanTables { get; set; } + + public JpegQuantizationTableConfig[] QuantizationTables { get; set; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 414f08248a..066e79bc0c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -50,6 +50,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private HuffmanScanEncoder scanEncoder; + public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4]; + /// /// The output stream. All attempted writes after the first error become no-ops. /// @@ -95,10 +97,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg ImageMetadata metadata = image.Metadata; JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); - // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that - // Initialize the quantization tables. - this.InitQuantizationTables(this.frameConfig.Components.Length, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); - // Write the Start Of Image marker. this.WriteStartOfImage(); @@ -117,21 +115,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteApp14Marker(); } - // Write the quantization tables. - this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); - // Write the image dimensions. this.WriteStartOfFrame(image.Width, image.Height, this.frameConfig); // Write the Huffman tables. this.WriteDefineHuffmanTables(this.scanConfig.HuffmanTables); + // Write the quantization tables. + this.InitQuantizationTables(this.scanConfig.QuantizationTables, jpegMetadata); + // Write the scan header. this.WriteStartOfScan(this.frameConfig.Components.Length, this.frameConfig.Components); var frame = new Components.Encoder.JpegFrame(this.frameConfig, Configuration.Default.MemoryAllocator, image, GetTargetColorSpace(this.frameConfig.ColorType)); - var quantTables = new Block8x8F[] { luminanceQuantTable, chrominanceQuantTable }; - this.scanEncoder.EncodeInterleavedScan(frame, image, quantTables, Configuration.Default, cancellationToken); + this.scanEncoder.EncodeInterleavedScan(frame, image, this.QuantizationTables, Configuration.Default, cancellationToken); // Write the End Of Image marker. this.WriteEndOfImageMarker(); @@ -281,26 +278,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Writes the Define Huffman Table marker and tables. /// /// The number of components to write. - private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tables) + private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs) { int markerlen = 2; - for (int i = 0; i < tables.Length; i++) + for (int i = 0; i < tableConfigs.Length; i++) { - markerlen += 1 + 16 + tables[i].TableSpec.Values.Length; + markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length; } this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen); - for (int i = 0; i < tables.Length; i++) + for (int i = 0; i < tableConfigs.Length; i++) { - JpegHuffmanTableConfig table = tables[i]; + JpegHuffmanTableConfig tableConfig = tableConfigs[i]; - int header = (table.Class << 4) | table.DestinationIndex; + int header = (tableConfig.Class << 4) | tableConfig.DestinationIndex; this.outputStream.WriteByte((byte)header); - this.outputStream.Write(table.TableSpec.Count); - this.outputStream.Write(table.TableSpec.Values); + this.outputStream.Write(tableConfig.Table.Count); + this.outputStream.Write(tableConfig.Table.Values); - this.scanEncoder.BuildHuffmanTable(table); + this.scanEncoder.BuildHuffmanTable(tableConfig); } } @@ -749,37 +746,42 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Jpeg metadata instance. /// Output luminance quantization table. /// Output chrominance quantization table. - private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) + private void InitQuantizationTables(JpegQuantizationTableConfig[] configs, JpegMetadata metadata) { - int lumaQuality; - int chromaQuality; - if (this.quality.HasValue) - { - lumaQuality = this.quality.Value; - chromaQuality = this.quality.Value; - } - else - { - lumaQuality = metadata.LuminanceQuality; - chromaQuality = metadata.ChrominanceQuality; - } + int dataLen = configs.Length * (1 + Block8x8F.Size); - // Luminance - lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + // Marker + quantization table lengths. + int markerlen = 2 + dataLen; + this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); + + byte[] buffer = new byte[dataLen]; + int offset = 0; - // Chrominance - chrominanceQuantTable = default; - if (componentCount > 1) + for (int i = 0; i < configs.Length; i++) { - chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + JpegQuantizationTableConfig config = configs[i]; - if (!this.colorType.HasValue) + // write to the output stream + buffer[offset++] = (byte)config.DestinationIndex; + for (int j = 0; j < Block8x8F.Size; j++) { - this.colorType = chromaQuality >= 91 ? JpegEncodingMode.YCbCrRatio444 : JpegEncodingMode.YCbCrRatio420; + buffer[offset++] = (byte)(uint)config.Table[ZigZag.ZigZagOrder[j]]; } + + // apply scaling and save into buffer + int quality = GetQualityForTable(config.DestinationIndex, this.quality, metadata); + this.QuantizationTables[config.DestinationIndex] = Quantization.ScaleQuantizationTable(quality, config.Table); } + + // write filled buffer to the stream + this.outputStream.Write(buffer); + + static int GetQualityForTable(int destIndex, int? encoderQuality, JpegMetadata metadata) => destIndex switch + { + 0 => encoderQuality ?? metadata.LuminanceQuality, + 1 => encoderQuality ?? metadata.ChrominanceQuality, + _ => encoderQuality ?? metadata.Quality, + }; } } }