From b16301b631520a778f2af29acfbeeeebad177bcc Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 10:19:09 +0300 Subject: [PATCH 01/40] Simplified quantization table scaling in the encoder --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 135048aa4e..60c209c524 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -702,19 +702,8 @@ private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) 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; + int scaled = ((unscaledQuant[j] * scale) + 50) / 100; + quant[j] = Math.Clamp(scaled, 1, 255); } } } From 51e13667909bdd8dc0ad89ecb28ec257495e588e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 10:19:22 +0300 Subject: [PATCH 02/40] Added debug guard to the estimate quality --- .../Formats/Jpeg/Components/Decoder/QualityEvaluator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs index 938459b88e..8c014ecda5 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder @@ -78,6 +78,8 @@ internal static class QualityEvaluator /// The . public static int EstimateQuality(Block8x8F[] quantizationTables) { + DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); + int quality = 75; float sum = 0; @@ -141,4 +143,4 @@ public static int EstimateQuality(Block8x8F[] quantizationTables) return quality; } } -} \ No newline at end of file +} From 1d781da19326ef775a000ebae05663c6d4b981a2 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 12:45:18 +0300 Subject: [PATCH 03/40] Added comments to DQT marker parser, DQT exceptions now provide better info messages --- .../Jpeg/Components/Decoder/JpegComponent.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 45 +++++++++---------- .../Formats/Jpeg/JpegThrowHelper.cs | 5 ++- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index 614e96e54a..95223c4445 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -32,7 +32,7 @@ public JpegComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id, if (quantizationTableIndex > 3) { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTableIndex(quantizationTableIndex); } this.QuantizationTableIndex = quantizationTableIndex; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 922e9797cb..3896aa2930 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -755,26 +755,29 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in { while (remaining > 0) { - bool done = false; - remaining--; + // 1 byte: quantization table spec + // bit 0..3: table index (0..3) + // bit 4..7: table precision (0 = 8 bit, 1 = 16 bit) int quantizationTableSpec = stream.ReadByte(); int tableIndex = quantizationTableSpec & 15; + int tablePrecision = quantizationTableSpec >> 4; - // Max index. 4 Tables max. + // Validate: if (tableIndex > 3) { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTableIndex(tableIndex); } - switch (quantizationTableSpec >> 4) + remaining--; + switch (tablePrecision) { + // 8 bit values case 0: { - // 8 bit values + // Validate: 8 bit table needs exactly 64 bytes if (remaining < 64) { - done = true; - break; + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } stream.Read(this.temp, 0, 64); @@ -785,16 +788,17 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in { table[j] = this.temp[j]; } + + break; } - break; + // 16 bit values case 1: { - // 16 bit values + // Validate: 16 bit table needs exactly 128 bytes if (remaining < 128) { - done = true; - break; + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } stream.Read(this.temp, 0, 128); @@ -805,26 +809,17 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in { table[j] = (this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]; } - } - break; + break; + } + // Unknown precision - error default: { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTablePrecision(tablePrecision); break; } } - - if (done) - { - break; - } - } - - if (remaining != 0) - { - JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } this.Metadata.GetFormatMetadata(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); diff --git a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index cc75870e19..1b5362275d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -36,7 +36,10 @@ public static void ThrowNotImplementedException(string errorMessage) public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadQuantizationTable() => throw new InvalidImageContentException("Bad Quantization Table index."); + public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowBadQuantizationTablePrecision(int precision) => throw new InvalidImageContentException($"Unknown Quantization Table precision {precision}."); [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadSampling() => throw new InvalidImageContentException("Bad sampling factor."); From 15d77ddf26a1adb90ef19f0d84413c2145ae289f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 17:42:11 +0300 Subject: [PATCH 04/40] EstimateQuality now return if given tables are standard --- .../Components/Decoder/QualityEvaluator.cs | 24 ++++++++++++------- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs index 8c014ecda5..e9d67d1ba9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs @@ -72,15 +72,17 @@ internal static class QualityEvaluator }; /// - /// Returns an estimated quality of the image based on the quantization tables. + /// Returns a jpeg quality parameter based on the quantization tables. /// /// The quantization tables. - /// The . - public static int EstimateQuality(Block8x8F[] quantizationTables) + /// Jpeg quality parameter + /// indicating if given quantization tables are equal to standard ITU spec. + public static bool EstimateQuality(Block8x8F[] quantizationTables, out int quality) { DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); - int quality = 75; + quality = 75; + float sum = 0; for (int i = 0; i < quantizationTables.Length; i++) @@ -115,9 +117,11 @@ public static int EstimateQuality(Block8x8F[] quantizationTables) continue; } - if (((quality <= Hash[i]) && (sum <= Sums[i])) || (i >= 50)) + bool sumHashCondition = (quality <= Hash[i]) && (sum <= Sums[i]); + if (sumHashCondition || (i >= 50)) { - return i + 1; + quality = i + 1; + return sumHashCondition; } } } @@ -132,15 +136,17 @@ public static int EstimateQuality(Block8x8F[] quantizationTables) continue; } - if (((quality <= Hash1[i]) && (sum <= Sums1[i])) || (i >= 50)) + bool sumHashCondition = (quality <= Hash1[i]) && (sum <= Sums1[i]); + if (sumHashCondition || (i >= 50)) { - return i + 1; + quality = i + 1; + return sumHashCondition; } } } } - return quality; + return false; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 8131f74d26..b95dd5644e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -18,7 +18,7 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int? Quality { get; set; } + public int? Quality { get; set; } = 75; /// /// Gets or sets the subsample ration, that will be used to encode the image. From 664d7f366f6eafda5f6c590ab316c15430e7e21a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 22:47:08 +0300 Subject: [PATCH 05/40] Initial new quality estimator --- .../Components/Decoder/QualityEvaluator.cs | 152 ------------------ .../Formats/Jpeg/Components/Quantization.cs | 128 +++++++++++++++ .../Formats/Jpeg/JpegDecoderCore.cs | 39 ++++- .../Formats/Jpeg/JpegEncoderCore.cs | 40 +---- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 36 ++++- 5 files changed, 199 insertions(+), 196 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Quantization.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs deleted file mode 100644 index e9d67d1ba9..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - /// - /// Provides methods to evaluate the quality of an image. - /// Ported from - /// - internal static class QualityEvaluator - { - private static readonly int[] Hash = new int[101] - { - 1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645, - 632, 623, 613, 607, 600, 594, 589, 585, 581, 571, - 555, 542, 529, 514, 494, 474, 457, 439, 424, 410, - 397, 386, 373, 364, 351, 341, 334, 324, 317, 309, - 299, 294, 287, 279, 274, 267, 262, 257, 251, 247, - 243, 237, 232, 227, 222, 217, 213, 207, 202, 198, - 192, 188, 183, 177, 173, 168, 163, 157, 153, 148, - 143, 139, 132, 128, 125, 119, 115, 108, 104, 99, - 94, 90, 84, 79, 74, 70, 64, 59, 55, 49, - 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, - 0 - }; - - private static readonly int[] Sums = new int[101] - { - 32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, - 27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946, - 23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998, - 16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702, - 12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208, - 9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458, - 8270, 8084, 7896, 7710, 7527, 7347, 7156, 6977, 6788, - 6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128, - 4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698, 3509, - 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846, - 1666, 1483, 1297, 1109, 927, 735, 554, 375, 201, - 128, 0 - }; - - private static readonly int[] Hash1 = new int[101] - { - 510, 505, 422, 380, 355, 338, 326, 318, 311, 305, - 300, 297, 293, 291, 288, 286, 284, 283, 281, 280, - 279, 278, 277, 273, 262, 251, 243, 233, 225, 218, - 211, 205, 198, 193, 186, 181, 177, 172, 168, 164, - 158, 156, 152, 148, 145, 142, 139, 136, 133, 131, - 129, 126, 123, 120, 118, 115, 113, 110, 107, 105, - 102, 100, 97, 94, 92, 89, 87, 83, 81, 79, - 76, 74, 70, 68, 66, 63, 61, 57, 55, 52, - 50, 48, 44, 42, 39, 37, 34, 31, 29, 26, - 24, 21, 18, 16, 13, 11, 8, 6, 3, 2, - 0 - }; - - private static readonly int[] Sums1 = new int[101] - { - 16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859, - 12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679, - 9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823, - 6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086, - 4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092, - 3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396, - 3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727, - 2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068, - 1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398, - 1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736, - 667, 592, 518, 441, 369, 292, 221, 151, 86, - 64, 0 - }; - - /// - /// Returns a jpeg quality parameter based on the quantization tables. - /// - /// The quantization tables. - /// Jpeg quality parameter - /// indicating if given quantization tables are equal to standard ITU spec. - public static bool EstimateQuality(Block8x8F[] quantizationTables, out int quality) - { - DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); - - quality = 75; - - float sum = 0; - - for (int i = 0; i < quantizationTables.Length; i++) - { - ref Block8x8F qTable = ref quantizationTables[i]; - - if (!qTable.Equals(default)) - { - for (int j = 0; j < Block8x8F.Size; j++) - { - sum += qTable[j]; - } - } - } - - ref Block8x8F qTable0 = ref quantizationTables[0]; - ref Block8x8F qTable1 = ref quantizationTables[1]; - - if (!qTable0.Equals(default)) - { - if (!qTable1.Equals(default)) - { - quality = (int)(qTable0[2] - + qTable0[53] - + qTable1[0] - + qTable1[Block8x8F.Size - 1]); - - for (int i = 0; i < 100; i++) - { - if (quality < Hash[i] && sum < Sums[i]) - { - continue; - } - - bool sumHashCondition = (quality <= Hash[i]) && (sum <= Sums[i]); - if (sumHashCondition || (i >= 50)) - { - quality = i + 1; - return sumHashCondition; - } - } - } - else - { - quality = (int)(qTable0[2] + qTable0[53]); - - for (int i = 0; i < 100; i++) - { - if (quality < Hash1[i] && sum < Sums1[i]) - { - continue; - } - - bool sumHashCondition = (quality <= Hash1[i]) && (sum <= Sums1[i]); - if (sumHashCondition || (i >= 50)) - { - quality = i + 1; - return sumHashCondition; - } - } - } - } - - return false; - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs new file mode 100644 index 0000000000..3471558054 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Provides methods and properties related to jpeg quantization. + /// + internal static class Quantization + { + /// + /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// + /// + /// Jpeg does not define either 'quality' nor 'standard quantization table' properties + /// so this is purely a practical value derived from tests. + /// + public const double StandardLuminanceTableVarianceThreshold = 10.0; + + /// + /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// + /// + /// Jpeg does not define either 'quality' nor 'standard quantization table' properties + /// so this is purely a practical value derived from tests. + /// + public const double StandardChrominanceTableVarianceThreshold = 10.0; + + /// + /// Gets the unscaled luminance quantization table in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from ITU 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 + public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + { + 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 chrominance quantization table in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from ITU 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 + public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + { + 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, + }; + + // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) + { + // This method can be SIMD'ified if standard table is injected as Block8x8F + // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8 + double comparePercent; + double sumPercent = 0; + double sumPercentSqr = 0; + + bool allOnes = true; + + for (int i = 0; i < Block8x8F.Size; i++) + { + float coeff = table[i]; + int coeffInteger = (int)coeff; + + // coefficients are actually int16 casted to float numbers so there's no truncating error + if (coeffInteger != 0) + { + comparePercent = 100.0 * (table[i] / target[i]); + } + else + { + comparePercent = 999.99; + } + + sumPercent += comparePercent; + sumPercentSqr += comparePercent * comparePercent; + + // Check just in case entire table are ones (Quality 100) + if (coeffInteger != 1) + { + allOnes = false; + } + } + + // Perform some statistical analysis of the quality factor + // to determine the likelihood of the current quantization + // table being a scaled version of the "standard" tables. + // If the variance is high, it is unlikely to be the case. + sumPercent /= 64.0; + sumPercentSqr /= 64.0; + variance = sumPercentSqr - (sumPercent * sumPercent); + + // Generate the equivalent IJQ "quality" factor + if (allOnes) + { + quality = 100; + } + else if (sumPercent <= 100.0) + { + quality = (200 - sumPercent) / 2; + } + else + { + quality = 5000.0 / sumPercent; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 3896aa2930..b86772d81c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -753,6 +753,8 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining) /// private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { + JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + while (remaining > 0) { // 1 byte: quantization table spec @@ -769,6 +771,9 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in } remaining--; + + // Decoding single 8x8 table + ref Block8x8F table = ref this.QuantizationTables[tableIndex]; switch (tablePrecision) { // 8 bit values @@ -783,7 +788,6 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in stream.Read(this.temp, 0, 64); remaining -= 64; - ref Block8x8F table = ref this.QuantizationTables[tableIndex]; for (int j = 0; j < 64; j++) { table[j] = this.temp[j]; @@ -804,7 +808,6 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in stream.Read(this.temp, 0, 128); remaining -= 128; - ref Block8x8F table = ref this.QuantizationTables[tableIndex]; for (int j = 0; j < 64; j++) { table[j] = (this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]; @@ -820,9 +823,37 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in break; } } - } - this.Metadata.GetFormatMetadata(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); + // Estimating quality + switch (tableIndex) + { + // luminance table + case 0: + { + Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); + jpegMetadata.LumaQuality = quality; + if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) + { + jpegMetadata.lumaQuantizationTable = table.RoundAsInt16Block(); + } + + break; + } + + // chrominance table + case 1: + { + Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); + jpegMetadata.ChromaQuality = quality; + if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) + { + jpegMetadata.chromaQuantizationTable = table.RoundAsInt16Block(); + } + + break; + } + } + } } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 60c209c524..f3fddd9e0d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -64,44 +64,6 @@ public JpegEncoderCore(IJpegEncoderOptions options) this.colorType = options.ColorType; } - /// - /// 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, - }; - /// /// Encode writes the image to the jpeg baseline format with the given options. /// @@ -698,7 +660,7 @@ private void WriteMarkerHeader(byte marker, int length) 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; + ReadOnlySpan unscaledQuant = (i == 0) ? Quantization.UnscaledQuant_Luminance : Quantization.UnscaledQuant_Chrominance; for (int j = 0; j < Block8x8F.Size; j++) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 9670d167e0..0a05aac170 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + namespace SixLabors.ImageSharp.Formats.Jpeg { /// @@ -8,6 +11,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? lumaQuantizationTable; + + /// + /// Luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? chromaQuantizationTable; + + internal double LumaQuality; + + internal double ChromaQuality; + /// /// Initializes a new instance of the class. /// @@ -23,12 +46,23 @@ private JpegMetadata(JpegMetadata other) { this.Quality = other.Quality; this.ColorType = other.ColorType; + this.lumaQuantizationTable = other.lumaQuantizationTable; + this.chromaQuantizationTable = other.chromaQuantizationTable; } /// /// Gets or sets the encoded quality. /// - public int Quality { get; set; } = 75; + public int Quality + { + get => (int)Math.Round((this.LumaQuality + this.ChromaQuality) / 2f); + set + { + double halfValue = value / 2.0; + this.LumaQuality = halfValue; + this.ChromaQuality = halfValue; + } + } /// /// Gets or sets the encoded quality. From 7772ff12ee136090711c20ed1742d7869a3d3ab6 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 22:57:04 +0300 Subject: [PATCH 06/40] Added comments, docs and improved estimation performances with intrinsics (not tested) --- .../Formats/Jpeg/Components/Block8x8F.cs | 41 +++++++++++++++++++ .../Formats/Jpeg/Components/Quantization.cs | 33 ++++++++------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 8 ++++ 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 8ca7b0c801..d55dfced72 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -830,5 +830,46 @@ public void TransposeInto(ref Block8x8F d) d.V7R.W = this.V7R.W; } } + + /// + /// Compares entire 8x8 block to a single scalar value. + /// + /// Value to compare to. + public bool EqualsToScalar(int value) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + const int equalityMask = unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_1111); + + var targetVector = Vector256.Create(value); + ref Vector256 blockStride = ref this.V0; + + for (int i = 0; i < RowCount; i++) + { + Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); + if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) + { + return false; + } + } + + return true; + } +#endif + { + ref float scalars = ref Unsafe.As(ref this); + + for (int i = 0; i < Size; i++) + { + if ((int)Unsafe.Add(ref scalars, i) != value) + { + return false; + } + } + + return true; + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 3471558054..0c9a0ca416 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -69,37 +69,44 @@ internal static class Quantization // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) { - // This method can be SIMD'ified if standard table is injected as Block8x8F - // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8 + // This method can be SIMD'ified if standard table is injected as Block8x8F. + // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. double comparePercent; double sumPercent = 0; double sumPercentSqr = 0; - bool allOnes = true; + // Corner case - all 1's => 100 quality + // It would fail to deduce using algorithm below without this check + if (table.EqualsToScalar(1)) + { + // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit'. + quality = 100; + variance = 0; + return; + } for (int i = 0; i < Block8x8F.Size; i++) { float coeff = table[i]; int coeffInteger = (int)coeff; - // coefficients are actually int16 casted to float numbers so there's no truncating error + // Coefficients are actually int16 casted to float numbers so there's no truncating error. if (coeffInteger != 0) { comparePercent = 100.0 * (table[i] / target[i]); } else { + // No 'valid' quantization table should contain zero at any position + // while this is okay to decode with, it will throw DivideByZeroException at encoding proces stage. + // Not sure what to do here, we can't throw as this technically correct + // but this will screw up the encoder. comparePercent = 999.99; } sumPercent += comparePercent; sumPercentSqr += comparePercent * comparePercent; - - // Check just in case entire table are ones (Quality 100) - if (coeffInteger != 1) - { - allOnes = false; - } } // Perform some statistical analysis of the quality factor @@ -111,11 +118,7 @@ public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan targe variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor - if (allOnes) - { - quality = 100; - } - else if (sumPercent <= 100.0) + if (sumPercent <= 100.0) { quality = (200 - sumPercent) / 2; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0a05aac170..1b43f26f07 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -46,8 +46,11 @@ private JpegMetadata(JpegMetadata other) { this.Quality = other.Quality; this.ColorType = other.ColorType; + this.lumaQuantizationTable = other.lumaQuantizationTable; this.chromaQuantizationTable = other.chromaQuantizationTable; + this.LumaQuality = other.LumaQuality; + this.ChromaQuality = other.ChromaQuality; } /// @@ -64,6 +67,11 @@ public int Quality } } + /// + /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables + /// + public bool ItuSpecQuantization => !this.lumaQuantizationTable.HasValue && !this.chromaQuantizationTable.HasValue; + /// /// Gets or sets the encoded quality. /// From ca29541e4bd3dfed8568e9c8990c209472538dba Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:19:44 +0300 Subject: [PATCH 07/40] Fixed standard quality logic --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 9 ++------- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 1b43f26f07..cd94c5d5f8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -58,13 +58,8 @@ private JpegMetadata(JpegMetadata other) /// public int Quality { - get => (int)Math.Round((this.LumaQuality + this.ChromaQuality) / 2f); - set - { - double halfValue = value / 2.0; - this.LumaQuality = halfValue; - this.ChromaQuality = halfValue; - } + get => (int)Math.Round(this.LumaQuality); + set => this.LumaQuality = value; } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index f47ae55220..9d4aea453b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -55,7 +55,7 @@ public partial class JpegDecoderTests { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, - { TestImages.Jpeg.Issues.IncorrectQuality845, 99 } + { TestImages.Jpeg.Issues.IncorrectQuality845, 98 } }; [Theory] From 2fd703d40346258b4a2b396becc599e100bd63d4 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:24:44 +0300 Subject: [PATCH 08/40] Fixed 75 default quality from the encoder --- 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 b95dd5644e..8131f74d26 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -18,7 +18,7 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int? Quality { get; set; } = 75; + public int? Quality { get; set; } /// /// Gets or sets the subsample ration, that will be used to encode the image. From ee424d4f8739d2e869f05d1c89aa43b4b22b7040 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:44:45 +0300 Subject: [PATCH 09/40] Revert "Simplified quantization table scaling in the encoder" This reverts commit b16301b631520a778f2af29acfbeeeebad177bcc. --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index f3fddd9e0d..8711483355 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -664,8 +664,19 @@ private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) for (int j = 0; j < Block8x8F.Size; j++) { - int scaled = ((unscaledQuant[j] * scale) + 50) / 100; - quant[j] = Math.Clamp(scaled, 1, 255); + int x = unscaledQuant[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + quant[j] = x; } } } From d50f4b51ca7e7242e25fa212088b84ef0b2dae1b Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:45:15 +0300 Subject: [PATCH 10/40] Fixed compilation errors --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 46 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index b86772d81c..613a1117c1 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -834,7 +834,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in jpegMetadata.LumaQuality = quality; if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) { - jpegMetadata.lumaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } break; @@ -847,7 +847,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in jpegMetadata.ChromaQuality = quality; if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) { - jpegMetadata.chromaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } break; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index cd94c5d5f8..51a65d96df 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,26 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - /// - /// Luminance qunatization table derived from jpeg image. - /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8? lumaQuantizationTable; - - /// - /// Luminance qunatization table derived from jpeg image. - /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8? chromaQuantizationTable; - - internal double LumaQuality; - - internal double ChromaQuality; - /// /// Initializes a new instance of the class. /// @@ -47,12 +27,32 @@ private JpegMetadata(JpegMetadata other) this.Quality = other.Quality; this.ColorType = other.ColorType; - this.lumaQuantizationTable = other.lumaQuantizationTable; - this.chromaQuantizationTable = other.chromaQuantizationTable; + this.LumaQuantizationTable = other.LumaQuantizationTable; + this.ChromaQuantizationTable = other.ChromaQuantizationTable; this.LumaQuality = other.LumaQuality; this.ChromaQuality = other.ChromaQuality; } + /// + /// Gets or sets luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? LumaQuantizationTable { get; set; } + + /// + /// Gets or sets chrominance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? ChromaQuantizationTable { get; set; } + + internal double LumaQuality { get; set; } + + internal double ChromaQuality { get; set; } + /// /// Gets or sets the encoded quality. /// @@ -65,7 +65,7 @@ public int Quality /// /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables /// - public bool ItuSpecQuantization => !this.lumaQuantizationTable.HasValue && !this.chromaQuantizationTable.HasValue; + public bool ItuSpecQuantization => !this.LumaQuantizationTable.HasValue && !this.ChromaQuantizationTable.HasValue; /// /// Gets or sets the encoded quality. From 54105e32391f3d84b07ae98aef4bb1dacd5eef6f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 14:38:10 +0300 Subject: [PATCH 11/40] Added docs to the metadata properties --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 46 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 613a1117c1..8d8ad3dad3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -831,7 +831,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in case 0: { Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); - jpegMetadata.LumaQuality = quality; + jpegMetadata.LuminanceQuality = quality; if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); @@ -844,7 +844,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in case 1: { Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); - jpegMetadata.ChromaQuality = quality; + jpegMetadata.ChrominanceQuality = quality; if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 51a65d96df..da435fc7ec 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -29,8 +29,8 @@ private JpegMetadata(JpegMetadata other) this.LumaQuantizationTable = other.LumaQuantizationTable; this.ChromaQuantizationTable = other.ChromaQuantizationTable; - this.LumaQuality = other.LumaQuality; - this.ChromaQuality = other.ChromaQuality; + this.LuminanceQuality = other.LuminanceQuality; + this.ChrominanceQuality = other.ChrominanceQuality; } /// @@ -49,24 +49,48 @@ private JpegMetadata(JpegMetadata other) /// internal Block8x8? ChromaQuantizationTable { get; set; } - internal double LumaQuality { get; set; } + /// + /// Gets or sets the jpeg luminance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-complient ITU quantization tables. + /// + public double LuminanceQuality { get; set; } - internal double ChromaQuality { get; set; } + /// + /// Gets or sets the jpeg chrominance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-complient ITU quantization tables. + /// + public double ChrominanceQuality { get; set; } + + /// + /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. + /// + public bool UsesStandardLuminanceTable => !this.LumaQuantizationTable.HasValue; + + /// + /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. + /// + public bool UsesStandardChrominanceTable => !this.ChromaQuantizationTable.HasValue; /// /// Gets or sets the encoded quality. /// + [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round(this.LumaQuality); - set => this.LumaQuality = value; + get => (int)Math.Round(this.LuminanceQuality + this.ChrominanceQuality); + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } } - /// - /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables - /// - public bool ItuSpecQuantization => !this.LumaQuantizationTable.HasValue && !this.ChromaQuantizationTable.HasValue; - /// /// Gets or sets the encoded quality. /// From 174628306c92b50664a961b0eff287106372b51d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 15:27:48 +0300 Subject: [PATCH 12/40] Added docs --- .../Formats/Jpeg/Components/Quantization.cs | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 0c9a0ca416..a7af76a65d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -13,7 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components internal static class Quantization { /// - /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// Threshold at which given luminance quantization table should be considered 'standard'. + /// Bigger the variance - more likely it to be a non-ITU complient table. /// /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties @@ -22,7 +23,8 @@ internal static class Quantization public const double StandardLuminanceTableVarianceThreshold = 10.0; /// - /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// Threshold at which given chrominance quantization table should be considered 'standard'. + /// Bigger the variance - more likely it to be a non-ITU complient table. /// /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties @@ -66,8 +68,21 @@ internal static class Quantization 99, 99, 99, 99, 99, 99, 99, 99, }; - // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 - public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) + /// Ported from JPEGsnoop: + /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + /// + /// Estimates jpeg quality based on quantization table. + /// + /// + /// This technically can be used with any given table but internal decoder code uses ITU spec tables: + /// and . + /// + /// Input quantization table. + /// Quantization to estimate against. + /// Variance threshold after which given table is considered non-complient. + /// Estimated quality + /// indicating if given table is target-complient + private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -80,10 +95,10 @@ public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan targe if (table.EqualsToScalar(1)) { // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. - // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit'. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. + // Quality=100 shouldn't be used in usual use case. quality = 100; - variance = 0; - return; + return true; } for (int i = 0; i < Block8x8F.Size; i++) @@ -115,7 +130,7 @@ public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan targe // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; sumPercentSqr /= 64.0; - variance = sumPercentSqr - (sumPercent * sumPercent); + double variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -126,6 +141,26 @@ public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan targe { quality = 5000.0 / sumPercent; } + + return variance <= varianceThreshold; } + + /// + /// Estimates jpeg luminance quality. + /// + /// Luminance quantization table. + /// Output jpeg quality. + /// indicating if given table is ITU-complient. + public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); + + /// + /// Estimates jpeg chrominance quality. + /// + /// Chrominance quantization table. + /// Output jpeg quality. + /// indicating if given table is ITU-complient. + public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); } } From 4b20325746098c265ae9af80e04cdda5f47b2c34 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 15:42:39 +0300 Subject: [PATCH 13/40] Commented obsolete attribute for quality --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index da435fc7ec..a768f66514 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -80,10 +80,10 @@ private JpegMetadata(JpegMetadata other) /// /// Gets or sets the encoded quality. /// - [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] + // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round(this.LuminanceQuality + this.ChrominanceQuality); + get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2); set { this.LuminanceQuality = value; From 2494131cfa6d37ac280b10965fea17b6d834d112 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 22:55:26 +0300 Subject: [PATCH 14/40] Fixed invalid quality estimation --- .../Formats/Jpeg/Components/Quantization.cs | 48 +++++++++++++++-- .../Formats/Jpeg/JpegDecoderCore.cs | 16 +++--- .../Formats/Jpeg/JpegEncoderCore.cs | 51 ++----------------- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index a7af76a65d..b87c538a93 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components @@ -20,7 +21,7 @@ internal static class Quantization /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - public const double StandardLuminanceTableVarianceThreshold = 10.0; + private const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -30,7 +31,7 @@ internal static class Quantization /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - public const double StandardChrominanceTableVarianceThreshold = 10.0; + private const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -41,7 +42,7 @@ internal static class Quantization // 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 - public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] { 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, @@ -59,7 +60,7 @@ internal static class Quantization // 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 - public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] { 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, @@ -82,7 +83,7 @@ internal static class Quantization /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -151,6 +152,7 @@ private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan targ /// Luminance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); @@ -160,7 +162,43 @@ public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out do /// Chrominance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int QualityToScale(int quality) + => quality < 50 ? 5000 / quality : 200 - (quality * 2); + + private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + { + Block8x8F table = default; + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = unscaledTable[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + table[j] = x; + } + + return table; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleLuminanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Luminance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleChrominanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Chrominance); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 8d8ad3dad3..09b40e09d4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,26 +830,30 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // luminance table case 0: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); - jpegMetadata.LuminanceQuality = quality; - if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) + // if quantization table is non-complient to stardard itu table + // we can't reacreate it later with calculated quality as this is an approximation + // so we save it in the metadata + if (!Quantization.EstimateLuminanceQuality(ref table, out double quality)) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.LuminanceQuality = quality; break; } // chrominance table case 1: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); - jpegMetadata.ChrominanceQuality = quality; - if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) + // if quantization table is non-complient to stardard itu table + // we can't reacreate it later with calculated quality as this is an approximation + // so we save it in the metadata + if (!Quantization.EstimateChrominanceQuality(ref table, out double quality)) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.ChrominanceQuality = quality; break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 8711483355..c829e0972a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -94,27 +94,11 @@ public void Encode(Image image, Stream stream, CancellationToken int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // 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); - } + Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(qlty); + Block8x8F chrominanceQuantTable = Quantization.ScaleChrominanceTable(qlty); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -138,10 +122,12 @@ public void Encode(Image image, Stream stream, CancellationToken var scanEncoder = new HuffmanScanEncoder(stream); if (this.colorType == JpegColorType.Luminance) { + // luminance quantization table only scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); } else { + // luminance and chrominance quantization tables switch (this.subsample) { case JpegSubsample.Ratio444: @@ -650,34 +636,5 @@ private void WriteMarkerHeader(byte marker, int length) 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) ? Quantization.UnscaledQuant_Luminance : Quantization.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; - } - } } } From 961e3b1d6471eef8cfebdb7113cccd28a9667390 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 01:14:14 +0300 Subject: [PATCH 15/40] Added tests for variance thresholds --- .../Formats/Jpeg/Components/Quantization.cs | 51 ++++++++++---- .../Formats/Jpg/QuantizationTests.cs | 68 +++++++++++++++++++ 2 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index b87c538a93..3087551d01 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -13,6 +13,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// internal static class Quantization { + /// + /// Upper bound (inclusive) for jpeg quality setting. + /// + public const int MaxQualityFactor = 100; + + /// + /// Lower bound (inclusive) for jpeg quality setting. + /// + public const int MinQualityFactor = 1; + + /// + /// Represents lowest quality setting which can be estimated with enough confidence. + /// Any quality below it results in a highly compressed jpeg image + /// which shouldn't use standard itu quantization tables for re-encoding. + /// + public const int QualityEstimationConfidenceThreshold = 25; + /// /// Threshold at which given luminance quantization table should be considered 'standard'. /// Bigger the variance - more likely it to be a non-ITU complient table. @@ -21,7 +38,7 @@ internal static class Quantization /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardLuminanceTableVarianceThreshold = 10.0; + public const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -31,7 +48,7 @@ internal static class Quantization /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardChrominanceTableVarianceThreshold = 10.0; + public const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -42,7 +59,7 @@ internal static class Quantization // 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[] + public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] { 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, @@ -60,7 +77,7 @@ internal static class Quantization // 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[] + public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] { 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, @@ -72,7 +89,7 @@ internal static class Quantization /// Ported from JPEGsnoop: /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 /// - /// Estimates jpeg quality based on quantization table. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// /// This technically can be used with any given table but internal decoder code uses ITU spec tables: @@ -80,10 +97,9 @@ internal static class Quantization /// /// Input quantization table. /// Quantization to estimate against. - /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -99,7 +115,7 @@ public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan targe // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. quality = 100; - return true; + return 0; } for (int i = 0; i < Block8x8F.Size; i++) @@ -131,7 +147,6 @@ public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan targe // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; sumPercentSqr /= 64.0; - double variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -143,28 +158,34 @@ public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan targe quality = 5000.0 / sumPercent; } - return variance <= varianceThreshold; + return sumPercentSqr - (sumPercent * sumPercent); } /// - /// Estimates jpeg luminance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Luminance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) - => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); + return variance <= StandardLuminanceTableVarianceThreshold; + } /// - /// Estimates jpeg chrominance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Chrominance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) - => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); + return variance <= StandardChrominanceTableVarianceThreshold; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) @@ -172,6 +193,8 @@ private static int QualityToScale(int quality) private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) { + DebugGuard.MustBeBetweenOrEqualTo(scale, MinQualityFactor, MaxQualityFactor, nameof(scale)); + Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs new file mode 100644 index 0000000000..1870c39fae --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using Xunit; +using Xunit.Abstractions; + +using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class QuantizationTests + { + public QuantizationTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Luminance() + { + this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Chrominance() + { + this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + } +} From d537ede70a43988a7b243f8077e771aece5f7f2f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 19:53:04 +0300 Subject: [PATCH 16/40] Updated thresholds --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 3087551d01..dc1801f6d0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -37,8 +37,10 @@ internal static class Quantization /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. + /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. + /// Actual value is 2.3629059983706604, truncated unsignificant part. /// - public const double StandardLuminanceTableVarianceThreshold = 10.0; + public const double StandardLuminanceTableVarianceThreshold = 2.36291; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -47,8 +49,10 @@ internal static class Quantization /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. + /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. + /// Actual value is 0.8949631033036098, truncated unsignificant part. /// - public const double StandardChrominanceTableVarianceThreshold = 10.0; + public const double StandardChrominanceTableVarianceThreshold = 0.894963; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each From e40313cc5eeda1da3b103c005aa00115ef998d1d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 20:19:14 +0300 Subject: [PATCH 17/40] Made quality metadata int, added tests for standard tables --- .../Formats/Jpeg/Components/Quantization.cs | 17 +++++--- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 6 +-- .../Formats/Jpg/QuantizationTests.cs | 42 +++++++++++++++---- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index dc1801f6d0..fb477dda85 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -28,7 +28,12 @@ internal static class Quantization /// Any quality below it results in a highly compressed jpeg image /// which shouldn't use standard itu quantization tables for re-encoding. /// - public const int QualityEstimationConfidenceThreshold = 25; + public const int QualityEstimationConfidenceLowerThreshold = 25; + + /// + /// Represents highest quality setting which can be estimated with enough confidence. + /// + public const int QualityEstimationConfidenceUpperThreshold = 98; /// /// Threshold at which given luminance quantization table should be considered 'standard'. @@ -103,7 +108,7 @@ internal static class Quantization /// Quantization to estimate against. /// Estimated quality /// indicating if given table is target-complient - public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality) + public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out int quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -155,11 +160,11 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) { - quality = (200 - sumPercent) / 2; + quality = (int)Math.Round((200 - sumPercent) / 2); } else { - quality = 5000.0 / sumPercent; + quality = (int)Math.Round(5000.0 / sumPercent); } return sumPercentSqr - (sumPercent * sumPercent); @@ -172,7 +177,7 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) + public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) { double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); return variance <= StandardLuminanceTableVarianceThreshold; @@ -185,7 +190,7 @@ public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out do /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) + public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) { double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); return variance <= StandardChrominanceTableVarianceThreshold; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 09b40e09d4..b58e99a104 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -833,7 +833,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // if quantization table is non-complient to stardard itu table // we can't reacreate it later with calculated quality as this is an approximation // so we save it in the metadata - if (!Quantization.EstimateLuminanceQuality(ref table, out double quality)) + if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } @@ -848,7 +848,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // if quantization table is non-complient to stardard itu table // we can't reacreate it later with calculated quality as this is an approximation // so we save it in the metadata - if (!Quantization.EstimateChrominanceQuality(ref table, out double quality)) + if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index a768f66514..e6183705de 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -56,7 +56,7 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public double LuminanceQuality { get; set; } + public int LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -65,7 +65,7 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public double ChrominanceQuality { get; set; } + public int ChrominanceQuality { get; set; } /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. @@ -83,7 +83,7 @@ private JpegMetadata(JpegMetadata other) // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2); + get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); set { this.LuminanceQuality = value; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 1870c39fae..8ed14bd818 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -20,8 +20,37 @@ public QuantizationTests(ITestOutputHelper output) private ITestOutputHelper Output { get; } - //[Fact(Skip = "Debug only, enable manually!")] [Fact] + public void QualityEstimationFromStandardEncoderTables_Luminance() + { + int firstIndex = JpegQuantization.QualityEstimationConfidenceLowerThreshold; + int lastIndex = JpegQuantization.QualityEstimationConfidenceUpperThreshold; + for (int quality = firstIndex; quality <= lastIndex; quality++) + { + Block8x8F table = JpegQuantization.ScaleLuminanceTable(quality); + bool isStrandard = JpegQuantization.EstimateLuminanceQuality(ref table, out int actualQuality); + + Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); + Assert.Equal(quality, actualQuality); + } + } + + [Fact] + public void QualityEstimationFromStandardEncoderTables_Chrominance() + { + int firstIndex = JpegQuantization.QualityEstimationConfidenceLowerThreshold; + int lastIndex = JpegQuantization.QualityEstimationConfidenceUpperThreshold; + for (int quality = firstIndex; quality <= lastIndex; quality++) + { + Block8x8F table = JpegQuantization.ScaleChrominanceTable(quality); + bool isStrandard = JpegQuantization.EstimateChrominanceQuality(ref table, out int actualQuality); + + Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); + Assert.Equal(quality, actualQuality); + } + } + + [Fact(Skip = "Debug only, enable manually!")] public void PrintVariancesFromStandardTables_Luminance() { this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); @@ -29,10 +58,10 @@ public void PrintVariancesFromStandardTables_Luminance() double minVariance = double.MaxValue; double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) { Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out double quality); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out int quality); minVariance = Math.Min(minVariance, variance); maxVariance = Math.Max(maxVariance, variance); @@ -43,18 +72,17 @@ public void PrintVariancesFromStandardTables_Luminance() this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); } - //[Fact(Skip = "Debug only, enable manually!")] - [Fact] + [Fact(Skip = "Debug only, enable manually!")] public void PrintVariancesFromStandardTables_Chrominance() { this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); double minVariance = double.MaxValue; double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) { Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out double quality); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out int quality); minVariance = Math.Min(minVariance, variance); maxVariance = Math.Max(maxVariance, variance); From 81b5e7fce0c008425e7939080f7c318d5c6b4b97 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 01:25:10 +0300 Subject: [PATCH 18/40] Removed obsolete attribute --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index e6183705de..0579e7b5e8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -80,7 +80,6 @@ private JpegMetadata(JpegMetadata other) /// /// Gets or sets the encoded quality. /// - // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); From cf1ad8edc2144a479850b53a0a7a76b02a861386 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 16:42:26 +0300 Subject: [PATCH 19/40] (WIP) quality --- .../Formats/Jpeg/IJpegEncoderOptions.cs | 13 +++++-- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 34 +++++++++++++++++- .../Formats/Jpeg/JpegEncoderCore.cs | 35 +++++++++++++------ 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index cceed407c2..d2921ad4c0 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -9,11 +9,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal interface IJpegEncoderOptions { /// - /// Gets the quality, that will be used to encode the image. Quality - /// index must be between 0 and 100 (compression from max to min). + /// Gets the quality, that will be used to encode the luminance data of the image. + /// Quality index must be between 0 and 100 (compression from max to min). /// /// The quality of the jpg image from 0 to 100. - int? Quality { get; } + int? LuminanceQuality { get; } + + /// + /// Gets the quality, that will be used to encode the chrominance data of the image. + /// Quality index must be between 0 and 100 (compression from max to min). + /// + /// The quality of the jpg image from 0 to 100. + int? ChrominanceQuality { get; } /// /// Gets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 8131f74d26..27597a0f99 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -18,7 +19,38 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int? Quality { get; set; } + public int? Quality + { + [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] + get + { + const int defaultQuality = 75; + + int lumaQuality = this.LuminanceQuality ?? defaultQuality; + int chromaQuality = this.LuminanceQuality ?? lumaQuality; + return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + } + + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } + } + + /// + /// Gets or sets the quality, that will be used to encode luminance image data. + /// Quality index must be between 0 and 100 (compression from max to min). + /// Defaults to 75. + /// + public int? LuminanceQuality { get; set; } + + /// + /// Gets or sets the quality, that will be used to encode chrominance image data. + /// Quality index must be between 0 and 100 (compression from max to min). + /// Defaults to 75. + /// + public int? ChrominanceQuality { get; set; } /// /// Gets or sets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index c829e0972a..4c81c58dd3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { + /// + /// Default JPEG encoding quality for both luminance and chominance tables. + /// + private const int DefaultQualityValue = 75; + /// /// The number of quantization tables. /// @@ -41,7 +46,12 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// /// The quality, that will be used to encode the image. /// - private readonly int? quality; + private readonly int? luminanceQuality; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly int? chrominanceQuality; /// /// Gets or sets the subsampling method to use. @@ -59,7 +69,8 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.quality = options.Quality; + this.luminanceQuality = options.LuminanceQuality; + this.chrominanceQuality = options.ChrominanceQuality; this.subsample = options.Subsample; this.colorType = options.ColorType; } @@ -86,19 +97,23 @@ public void Encode(Image image, Stream stream, CancellationToken this.outputStream = stream; ImageMetadata metadata = image.Metadata; + JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; - // System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. - int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); - this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // 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 = Quantization.ScaleLuminanceTable(qlty); - Block8x8F chrominanceQuantTable = Quantization.ScaleChrominanceTable(qlty); + // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that + int lumaQuality = Numerics.Clamp(this.luminanceQuality ?? jpegMetadata.LuminanceQuality, 1, 100); + Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + Block8x8F chrominanceQuantTable = default; + if (componentCount > 1) + { + int chromaQuality = Numerics.Clamp(this.chrominanceQuality ?? jpegMetadata.ChrominanceQuality, 1, 100); + this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); From eaeab7a03ae88e92c503d32725907783d404425e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 17:13:17 +0300 Subject: [PATCH 20/40] (WIP) quality --- .../Formats/Jpeg/JpegEncoderCore.cs | 47 ++++++++++++++----- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 16 +++++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 4c81c58dd3..593937b929 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -102,18 +102,9 @@ public void Encode(Image image, Stream stream, CancellationToken // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; - // Initialize the quantization tables. // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that - int lumaQuality = Numerics.Clamp(this.luminanceQuality ?? jpegMetadata.LuminanceQuality, 1, 100); - Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); - Block8x8F chrominanceQuantTable = default; - if (componentCount > 1) - { - int chromaQuality = Numerics.Clamp(this.chrominanceQuality ?? jpegMetadata.ChrominanceQuality, 1, 100); - this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); - } + // Initialize the quantization tables. + this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -651,5 +642,39 @@ private void WriteMarkerHeader(byte marker, int length) this.buffer[3] = (byte)(length & 0xff); this.outputStream.Write(this.buffer, 0, 4); } + + /// + /// Initializes quntization tables. + /// + /// Color components count. + /// Jpeg metadata instance. + /// Output luminance quantization table. + /// Output chrominance quantization table. + private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) + { + // We take quality values in a hierarchical order: + // 1. Check if encoder has set quality + // 2. Check if metadata has special table for encoding + // 3. Check if metadata has set quality + // 4. Take default quality value - 75 + int lumaQuality = Numerics.Clamp( + this.luminanceQuality ?? metadata.LuminanceQuality ?? DefaultQualityValue, + min: 1, + max: 100); + + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + chrominanceQuantTable = default; + if (componentCount > 1) + { + int chromaQuality = Numerics.Clamp( + this.chrominanceQuality ?? metadata.ChrominanceQuality ?? DefaultQualityValue, + min: 1, + max: 100); + + this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0579e7b5e8..8b3332ef8b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -24,7 +24,6 @@ public JpegMetadata() /// The metadata to create an instance from. private JpegMetadata(JpegMetadata other) { - this.Quality = other.Quality; this.ColorType = other.ColorType; this.LumaQuantizationTable = other.LumaQuantizationTable; @@ -56,7 +55,7 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int LuminanceQuality { get; set; } + public int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -65,7 +64,7 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int ChrominanceQuality { get; set; } + public int? ChrominanceQuality { get; set; } /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. @@ -82,7 +81,16 @@ private JpegMetadata(JpegMetadata other) /// public int Quality { - get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); + [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] + get + { + const int defaultQuality = 75; + + int lumaQuality = this.LuminanceQuality ?? defaultQuality; + int chromaQuality = this.LuminanceQuality ?? lumaQuality; + return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + } + set { this.LuminanceQuality = value; From 3fdefa7aeff7e9be1ed62911af315210a64bfaed Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 17:29:49 +0300 Subject: [PATCH 21/40] Encoder now uses appropriate quality or provied quantization tables --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- .../Formats/Jpeg/JpegEncoderCore.cs | 66 ++++++++++++++----- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 8 +-- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index b58e99a104..cf21dd226b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -835,7 +835,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // so we save it in the metadata if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) { - jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.LuminanceQuantizationTable = table; } jpegMetadata.LuminanceQuality = quality; @@ -850,7 +850,7 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // so we save it in the metadata if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) { - jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.ChromaQuantizationTable = table; } jpegMetadata.ChrominanceQuality = quality; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 593937b929..fea24111ca 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -646,34 +646,66 @@ private void WriteMarkerHeader(byte marker, int length) /// /// Initializes quntization tables. /// + /// + /// We take quality values in a hierarchical order: + /// 1. Check if encoder has set quality + /// 2. Check if metadata has special table for encoding + /// 3. Check if metadata has set quality + /// 4. Take default quality value - 75 + /// /// Color components count. /// Jpeg metadata instance. /// Output luminance quantization table. /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - // We take quality values in a hierarchical order: - // 1. Check if encoder has set quality - // 2. Check if metadata has special table for encoding - // 3. Check if metadata has set quality - // 4. Take default quality value - 75 - int lumaQuality = Numerics.Clamp( - this.luminanceQuality ?? metadata.LuminanceQuality ?? DefaultQualityValue, - min: 1, - max: 100); - - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + // encoder quality + if (this.luminanceQuality.HasValue) + { + int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + } + + // non-standard table + else if (metadata.LuminanceQuantizationTable.HasValue) + { + luminanceQuantTable = metadata.LuminanceQuantizationTable.Value; + } + + // metadata or default quality + else + { + int lumaQuality = Numerics.Clamp(metadata.LuminanceQuality ?? DefaultQualityValue, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + } + chrominanceQuantTable = default; if (componentCount > 1) { - int chromaQuality = Numerics.Clamp( - this.chrominanceQuality ?? metadata.ChrominanceQuality ?? DefaultQualityValue, - min: 1, - max: 100); + int chromaQuality; - this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + // encoder quality + if (this.chrominanceQuality.HasValue) + { + chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); + chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); + } + + // non-standard table + else if (metadata.ChromaQuantizationTable.HasValue) + { + chromaQuality = metadata.ChrominanceQuality.Value; + chrominanceQuantTable = metadata.ChromaQuantizationTable.Value; + } + + // metadata or default quality + else + { + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? DefaultQualityValue, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 8b3332ef8b..4a58c6946c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -26,7 +26,7 @@ private JpegMetadata(JpegMetadata other) { this.ColorType = other.ColorType; - this.LumaQuantizationTable = other.LumaQuantizationTable; + this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; this.ChromaQuantizationTable = other.ChromaQuantizationTable; this.LuminanceQuality = other.LuminanceQuality; this.ChrominanceQuality = other.ChrominanceQuality; @@ -38,7 +38,7 @@ private JpegMetadata(JpegMetadata other) /// /// Would be null if jpeg was encoded using table from ITU spec /// - internal Block8x8? LumaQuantizationTable { get; set; } + internal Block8x8F? LuminanceQuantizationTable { get; set; } /// /// Gets or sets chrominance qunatization table derived from jpeg image. @@ -46,7 +46,7 @@ private JpegMetadata(JpegMetadata other) /// /// Would be null if jpeg was encoded using table from ITU spec /// - internal Block8x8? ChromaQuantizationTable { get; set; } + internal Block8x8F? ChromaQuantizationTable { get; set; } /// /// Gets or sets the jpeg luminance quality. @@ -69,7 +69,7 @@ private JpegMetadata(JpegMetadata other) /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. /// - public bool UsesStandardLuminanceTable => !this.LumaQuantizationTable.HasValue; + public bool UsesStandardLuminanceTable => !this.LuminanceQuantizationTable.HasValue; /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. From 376d190d2a81fb672446a7eb3c30d530f003a1d8 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 21:05:04 +0300 Subject: [PATCH 22/40] Fixed encoder metadata usage --- .../Formats/Jpeg/JpegEncoderCore.cs | 32 +------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 80 ++++++++++++------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index fea24111ca..bc3a5f1b56 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -23,11 +23,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { - /// - /// Default JPEG encoding quality for both luminance and chominance tables. - /// - private const int DefaultQualityValue = 75; - /// /// The number of quantization tables. /// @@ -659,50 +654,29 @@ private void WriteMarkerHeader(byte marker, int length) /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - // encoder quality if (this.luminanceQuality.HasValue) { int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); } - - // non-standard table - else if (metadata.LuminanceQuantizationTable.HasValue) - { - luminanceQuantTable = metadata.LuminanceQuantizationTable.Value; - } - - // metadata or default quality else { - int lumaQuality = Numerics.Clamp(metadata.LuminanceQuality ?? DefaultQualityValue, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + luminanceQuantTable = metadata.LuminanceQuantizationTable; } chrominanceQuantTable = default; if (componentCount > 1) { int chromaQuality; - - // encoder quality if (this.chrominanceQuality.HasValue) { chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); } - - // non-standard table - else if (metadata.ChromaQuantizationTable.HasValue) - { - chromaQuality = metadata.ChrominanceQuality.Value; - chrominanceQuantTable = metadata.ChromaQuantizationTable.Value; - } - - // metadata or default quality else { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? DefaultQualityValue, 1, 100); - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + chromaQuality = metadata.ChrominanceQuality; + chrominanceQuantTable = metadata.ChromaQuantizationTable; } this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 4a58c6946c..6b7f42bb27 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,6 +11,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Default JPEG quality for both luminance and chominance tables. + /// + private const int DefaultQualityValue = 75; + + private Block8x8F? lumaQuantTable; + private Block8x8F? chromaQuantTable; + + private int? lumaQuality; + private int? chromaQuality; + /// /// Initializes a new instance of the class. /// @@ -33,20 +44,40 @@ private JpegMetadata(JpegMetadata other) } /// - /// Gets or sets luminance qunatization table derived from jpeg image. + /// Gets or sets luminance qunatization table for jpeg image. /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8F? LuminanceQuantizationTable { get; set; } + internal Block8x8F LuminanceQuantizationTable + { + get + { + if (this.lumaQuantTable.HasValue) + { + return this.lumaQuantTable.Value; + } + + return Quantization.ScaleLuminanceTable(this.LuminanceQuality); + } + + set => this.lumaQuantTable = value; + } /// - /// Gets or sets chrominance qunatization table derived from jpeg image. + /// Gets or sets chrominance qunatization table for jpeg image. /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8F? ChromaQuantizationTable { get; set; } + internal Block8x8F ChromaQuantizationTable + { + get + { + if (this.chromaQuantTable.HasValue) + { + return this.chromaQuantTable.Value; + } + + return Quantization.ScaleChrominanceTable(this.ChrominanceQuality); + } + + set => this.chromaQuantTable = value; + } /// /// Gets or sets the jpeg luminance quality. @@ -55,7 +86,11 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? LuminanceQuality { get; set; } + public int LuminanceQuality + { + get => this.lumaQuality ?? DefaultQualityValue; + set => this.lumaQuality = value; + } /// /// Gets or sets the jpeg chrominance quality. @@ -64,30 +99,21 @@ private JpegMetadata(JpegMetadata other) /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? ChrominanceQuality { get; set; } - - /// - /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. - /// - public bool UsesStandardLuminanceTable => !this.LuminanceQuantizationTable.HasValue; - - /// - /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. - /// - public bool UsesStandardChrominanceTable => !this.ChromaQuantizationTable.HasValue; + public int ChrominanceQuality + { + get => this.chromaQuality ?? DefaultQualityValue; + set => this.chromaQuality = value; + } /// /// Gets or sets the encoded quality. /// public int Quality { - [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] get { - const int defaultQuality = 75; - - int lumaQuality = this.LuminanceQuality ?? defaultQuality; - int chromaQuality = this.LuminanceQuality ?? lumaQuality; + int lumaQuality = this.lumaQuality ?? DefaultQualityValue; + int chromaQuality = this.chromaQuality ?? lumaQuality; return (int)Math.Round((lumaQuality + chromaQuality) / 2f); } From e9fddd0a48b2a9367c8c40d2a2b57ab1c08d7852 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 21 Jul 2021 00:27:31 +0300 Subject: [PATCH 23/40] Fixed subsample assignment in the encoder --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index bc3a5f1b56..9c66cfe0c3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -679,7 +679,10 @@ private void InitQuantizationTables(int componentCount, JpegMetadata metadata, o chrominanceQuantTable = metadata.ChromaQuantizationTable; } - this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + if (!this.subsample.HasValue) + { + this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + } } } } From a9a4f6f51d32682543a671914c70553b82e38b09 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 05:59:53 +0300 Subject: [PATCH 24/40] Removed luma/chroma quality from the encoder --- .../Formats/Jpeg/IJpegEncoderOptions.cs | 15 ++----- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 40 +------------------ .../Formats/Jpeg/JpegEncoderCore.cs | 4 +- 3 files changed, 8 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index d2921ad4c0..a9f564b450 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -9,18 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal interface IJpegEncoderOptions { /// - /// Gets the quality, that will be used to encode the luminance data of the image. - /// Quality index must be between 0 and 100 (compression from max to min). + /// Gets or sets the quality, that will be used to encode the image. Quality + /// index must be between 0 and 100 (compression from max to min). + /// Defaults to 75. /// - /// The quality of the jpg image from 0 to 100. - int? LuminanceQuality { get; } - - /// - /// Gets the quality, that will be used to encode the chrominance data of the image. - /// Quality index must be between 0 and 100 (compression from max to min). - /// - /// The quality of the jpg image from 0 to 100. - int? ChrominanceQuality { get; } + public int? Quality { get; set; } /// /// Gets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 27597a0f99..5e199b4204 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -14,43 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions { - /// - /// Gets or sets the quality, that will be used to encode the image. Quality - /// index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? Quality - { - [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] - get - { - const int defaultQuality = 75; - - int lumaQuality = this.LuminanceQuality ?? defaultQuality; - int chromaQuality = this.LuminanceQuality ?? lumaQuality; - return (int)Math.Round((lumaQuality + chromaQuality) / 2f); - } - - set - { - this.LuminanceQuality = value; - this.ChrominanceQuality = value; - } - } - - /// - /// Gets or sets the quality, that will be used to encode luminance image data. - /// Quality index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? LuminanceQuality { get; set; } - - /// - /// Gets or sets the quality, that will be used to encode chrominance image data. - /// Quality index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? ChrominanceQuality { get; set; } + /// + public int? Quality { get; set; } /// /// Gets or sets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 9c66cfe0c3..248772b287 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -64,8 +64,8 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.luminanceQuality = options.LuminanceQuality; - this.chrominanceQuality = options.ChrominanceQuality; + this.luminanceQuality = options.Quality; + this.chrominanceQuality = options.Quality; this.subsample = options.Subsample; this.colorType = options.ColorType; } From 5bdc5a0a1544d95cfc01d81312604a5e9e7331fe Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:35:22 +0300 Subject: [PATCH 25/40] Added remarks --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 6b7f42bb27..92f3a92ba0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -108,6 +108,10 @@ public int ChrominanceQuality /// /// Gets or sets the encoded quality. /// + /// + /// Note that jpeg image can have different quality for luminance and chrominance components. + /// This property return average for both qualities and sets both qualities to the given value. + /// public int Quality { get From 74534665dd476becce31d5601090969a688568aa Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:35:31 +0300 Subject: [PATCH 26/40] Fixed comments --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 92f3a92ba0..a4f968d7c1 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -129,7 +129,7 @@ public int Quality } /// - /// Gets or sets the encoded quality. + /// Gets or sets the color type. /// public JpegColorType? ColorType { get; set; } From 45292fbc09779ac8d7ed092987eda5088f344674 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:36:40 +0300 Subject: [PATCH 27/40] Fixed clamping --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 248772b287..69ac5de714 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -675,7 +675,7 @@ private void InitQuantizationTables(int componentCount, JpegMetadata metadata, o } else { - chromaQuality = metadata.ChrominanceQuality; + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality, 1, 100); chrominanceQuantTable = metadata.ChromaQuantizationTable; } From 404639ff8c7e3dad659841d04fb54bb47b017de7 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 23 Jul 2021 17:48:34 +0300 Subject: [PATCH 28/40] Final API improvements --- .../Formats/Jpeg/Components/Quantization.cs | 5 ++++ .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 28 ++++--------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index fb477dda85..fc602b7f85 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -23,6 +23,11 @@ internal static class Quantization /// public const int MinQualityFactor = 1; + /// + /// Default JPEG quality for both luminance and chominance tables. + /// + public const int DefaultQualityFactor = 75; + /// /// Represents lowest quality setting which can be estimated with enough confidence. /// Any quality below it results in a highly compressed jpeg image diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 69ac5de714..828e03de7f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -675,7 +675,7 @@ private void InitQuantizationTables(int componentCount, JpegMetadata metadata, o } else { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality, 1, 100); + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, 1, 100); chrominanceQuantTable = metadata.ChromaQuantizationTable; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index a4f968d7c1..0d95599e27 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,17 +11,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - /// - /// Default JPEG quality for both luminance and chominance tables. - /// - private const int DefaultQualityValue = 75; - private Block8x8F? lumaQuantTable; private Block8x8F? chromaQuantTable; - private int? lumaQuality; - private int? chromaQuality; - /// /// Initializes a new instance of the class. /// @@ -55,7 +47,7 @@ internal Block8x8F LuminanceQuantizationTable return this.lumaQuantTable.Value; } - return Quantization.ScaleLuminanceTable(this.LuminanceQuality); + return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor); } set => this.lumaQuantTable = value; @@ -73,7 +65,7 @@ internal Block8x8F ChromaQuantizationTable return this.chromaQuantTable.Value; } - return Quantization.ScaleChrominanceTable(this.ChrominanceQuality); + return Quantization.ScaleChrominanceTable(this.ChrominanceQuality ?? Quantization.DefaultQualityFactor); } set => this.chromaQuantTable = value; @@ -86,11 +78,7 @@ internal Block8x8F ChromaQuantizationTable /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int LuminanceQuality - { - get => this.lumaQuality ?? DefaultQualityValue; - set => this.lumaQuality = value; - } + public int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -99,11 +87,7 @@ public int LuminanceQuality /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int ChrominanceQuality - { - get => this.chromaQuality ?? DefaultQualityValue; - set => this.chromaQuality = value; - } + public int? ChrominanceQuality { get; set; } /// /// Gets or sets the encoded quality. @@ -116,8 +100,8 @@ public int Quality { get { - int lumaQuality = this.lumaQuality ?? DefaultQualityValue; - int chromaQuality = this.chromaQuality ?? lumaQuality; + int lumaQuality = this.LuminanceQuality ?? Quantization.DefaultQualityFactor; + int chromaQuality = this.ChrominanceQuality ?? lumaQuality; return (int)Math.Round((lumaQuality + chromaQuality) / 2f); } From 0e1aa6a9775d14d5ce7f930238a02171efd18071 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 27 Jul 2021 16:58:20 +0300 Subject: [PATCH 29/40] Made Quality nullable --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0d95599e27..77d27ee93b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -78,7 +78,7 @@ internal Block8x8F ChromaQuantizationTable /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? LuminanceQuality { get; set; } + internal int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -87,22 +87,34 @@ internal Block8x8F ChromaQuantizationTable /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? ChrominanceQuality { get; set; } + internal int? ChrominanceQuality { get; set; } /// /// Gets or sets the encoded quality. /// /// /// Note that jpeg image can have different quality for luminance and chrominance components. - /// This property return average for both qualities and sets both qualities to the given value. + /// This property returns maximum value of luma/chroma qualities. /// - public int Quality + public int? Quality { get { - int lumaQuality = this.LuminanceQuality ?? Quantization.DefaultQualityFactor; - int chromaQuality = this.ChrominanceQuality ?? lumaQuality; - return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + // Jpeg always has a luminance table thus it must have a luminance quality derived from it + if (!this.LuminanceQuality.HasValue) + { + return null; + } + + // Jpeg might not have a chrominance table + if (!this.ChrominanceQuality.HasValue) + { + return this.LuminanceQuality.Value; + } + + // Theoretically, luma quality would always be greater or equal to chroma quality + // But we've already encountered images which can have higher quality of chroma components + return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); } set From 7a99d6f8a318da7b9cddf3ec1600926b8917d2f5 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 27 Jul 2021 23:46:54 +0300 Subject: [PATCH 30/40] [Rollback] Removed table inheritance for jpeg re-encoding --- .../Formats/Jpeg/Components/Quantization.cs | 51 +++---------- .../Formats/Jpeg/JpegDecoderCore.cs | 20 +---- .../Formats/Jpeg/JpegEncoderCore.cs | 39 ++++------ src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 76 ++++++------------- .../Formats/Jpg/QuantizationTests.cs | 60 +-------------- 5 files changed, 56 insertions(+), 190 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index fc602b7f85..7e528d056d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -40,30 +40,6 @@ internal static class Quantization /// public const int QualityEstimationConfidenceUpperThreshold = 98; - /// - /// Threshold at which given luminance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 2.3629059983706604, truncated unsignificant part. - /// - public const double StandardLuminanceTableVarianceThreshold = 2.36291; - - /// - /// Threshold at which given chrominance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 0.8949631033036098, truncated unsignificant part. - /// - public const double StandardChrominanceTableVarianceThreshold = 0.894963; - /// /// Gets the unscaled luminance quantization table in zig-zag order. Each /// encoder copies and scales the tables according to its quality parameter. @@ -113,25 +89,24 @@ internal static class Quantization /// Quantization to estimate against. /// Estimated quality /// indicating if given table is target-complient - public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out int quality) + public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. double comparePercent; double sumPercent = 0; - double sumPercentSqr = 0; // Corner case - all 1's => 100 quality // It would fail to deduce using algorithm below without this check if (table.EqualsToScalar(1)) { // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. - // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' which will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. - quality = 100; - return 0; + return 100; } + int quality; for (int i = 0; i < Block8x8F.Size; i++) { float coeff = table[i]; @@ -152,7 +127,6 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar } sumPercent += comparePercent; - sumPercentSqr += comparePercent * comparePercent; } // Perform some statistical analysis of the quality factor @@ -160,7 +134,6 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar // table being a scaled version of the "standard" tables. // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; - sumPercentSqr /= 64.0; // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -172,7 +145,7 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar quality = (int)Math.Round(5000.0 / sumPercent); } - return sumPercentSqr - (sumPercent * sumPercent); + return quality; } /// @@ -182,11 +155,8 @@ public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan tar /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) - { - double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); - return variance <= StandardLuminanceTableVarianceThreshold; - } + public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); /// /// Estimates jpeg quality based on quantization table in zig-zag order. @@ -195,11 +165,8 @@ public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out in /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) - { - double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); - return variance <= StandardChrominanceTableVarianceThreshold; - } + public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index cf21dd226b..b6d5aafd16 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,30 +830,14 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in // luminance table case 0: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) - { - jpegMetadata.LuminanceQuantizationTable = table; - } - - jpegMetadata.LuminanceQuality = quality; + jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref table); break; } // chrominance table case 1: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) - { - jpegMetadata.ChromaQuantizationTable = table; - } - - jpegMetadata.ChrominanceQuality = quality; + jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref table); break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 828e03de7f..88d96f554d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -41,12 +41,7 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// /// The quality, that will be used to encode the image. /// - private readonly int? luminanceQuality; - - /// - /// The quality, that will be used to encode the image. - /// - private readonly int? chrominanceQuality; + private readonly int? quality; /// /// Gets or sets the subsampling method to use. @@ -64,8 +59,7 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.luminanceQuality = options.Quality; - this.chrominanceQuality = options.Quality; + this.quality = options.Quality; this.subsample = options.Subsample; this.colorType = options.ColorType; } @@ -654,30 +648,29 @@ private void WriteMarkerHeader(byte marker, int length) /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - if (this.luminanceQuality.HasValue) + int lumaQuality; + int chromaQuality; + if (this.quality.HasValue) { - int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + lumaQuality = this.quality.Value; + chromaQuality = this.quality.Value; } else { - luminanceQuantTable = metadata.LuminanceQuantizationTable; + lumaQuality = metadata.LuminanceQuality; + chromaQuality = metadata.ChrominanceQuality; } + // Luminance + lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + + // Chrominance chrominanceQuantTable = default; if (componentCount > 1) { - int chromaQuality; - if (this.chrominanceQuality.HasValue) - { - chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); - chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); - } - else - { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, 1, 100); - chrominanceQuantTable = metadata.ChromaQuantizationTable; - } + chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); if (!this.subsample.HasValue) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 77d27ee93b..1b17bdce7b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - private Block8x8F? lumaQuantTable; - private Block8x8F? chromaQuantTable; + private int? luminanceQuality; + private int? chrominanceQuality; /// /// Initializes a new instance of the class. @@ -29,46 +29,8 @@ private JpegMetadata(JpegMetadata other) { this.ColorType = other.ColorType; - this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; - this.ChromaQuantizationTable = other.ChromaQuantizationTable; - this.LuminanceQuality = other.LuminanceQuality; - this.ChrominanceQuality = other.ChrominanceQuality; - } - - /// - /// Gets or sets luminance qunatization table for jpeg image. - /// - internal Block8x8F LuminanceQuantizationTable - { - get - { - if (this.lumaQuantTable.HasValue) - { - return this.lumaQuantTable.Value; - } - - return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.lumaQuantTable = value; - } - - /// - /// Gets or sets chrominance qunatization table for jpeg image. - /// - internal Block8x8F ChromaQuantizationTable - { - get - { - if (this.chromaQuantTable.HasValue) - { - return this.chromaQuantTable.Value; - } - - return Quantization.ScaleChrominanceTable(this.ChrominanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.chromaQuantTable = value; + this.luminanceQuality = other.luminanceQuality; + this.chrominanceQuality = other.chrominanceQuality; } /// @@ -78,7 +40,11 @@ internal Block8x8F ChromaQuantizationTable /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? LuminanceQuality { get; set; } + internal int LuminanceQuality + { + get => this.luminanceQuality ?? Quantization.DefaultQualityFactor; + set => this.luminanceQuality = value; + } /// /// Gets or sets the jpeg chrominance quality. @@ -87,7 +53,11 @@ internal Block8x8F ChromaQuantizationTable /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? ChrominanceQuality { get; set; } + internal int ChrominanceQuality + { + get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor; + set => this.chrominanceQuality = value; + } /// /// Gets or sets the encoded quality. @@ -96,25 +66,29 @@ internal Block8x8F ChromaQuantizationTable /// Note that jpeg image can have different quality for luminance and chrominance components. /// This property returns maximum value of luma/chroma qualities. /// - public int? Quality + public int Quality { get { // Jpeg always has a luminance table thus it must have a luminance quality derived from it - if (!this.LuminanceQuality.HasValue) + if (!this.luminanceQuality.HasValue) { - return null; + return Quantization.DefaultQualityFactor; } - // Jpeg might not have a chrominance table - if (!this.ChrominanceQuality.HasValue) + int lumaQuality = this.luminanceQuality.Value; + + // Jpeg might not have a chrominance table - return luminance quality (grayscale images) + if (!this.chrominanceQuality.HasValue) { - return this.LuminanceQuality.Value; + return lumaQuality; } + int chromaQuality = this.chrominanceQuality.Value; + // Theoretically, luma quality would always be greater or equal to chroma quality // But we've already encountered images which can have higher quality of chroma components - return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); + return Math.Max(lumaQuality, chromaQuality); } set diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 8ed14bd818..2f673ef2f9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -13,13 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public class QuantizationTests { - public QuantizationTests(ITestOutputHelper output) - { - this.Output = output; - } - - private ITestOutputHelper Output { get; } - [Fact] public void QualityEstimationFromStandardEncoderTables_Luminance() { @@ -28,10 +21,9 @@ public void QualityEstimationFromStandardEncoderTables_Luminance() for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleLuminanceTable(quality); - bool isStrandard = JpegQuantization.EstimateLuminanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateLuminanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate luminance quality for standard table at quality level {quality}"); } } @@ -43,54 +35,10 @@ public void QualityEstimationFromStandardEncoderTables_Chrominance() for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleChrominanceTable(quality); - bool isStrandard = JpegQuantization.EstimateChrominanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateChrominanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate chrominance quality for standard table at quality level {quality}"); } } - - [Fact(Skip = "Debug only, enable manually!")] - public void PrintVariancesFromStandardTables_Luminance() - { - this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); - - double minVariance = double.MaxValue; - double maxVariance = double.MinValue; - - for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out int quality); - - minVariance = Math.Min(minVariance, variance); - maxVariance = Math.Max(maxVariance, variance); - - this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); - } - - this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); - } - - [Fact(Skip = "Debug only, enable manually!")] - public void PrintVariancesFromStandardTables_Chrominance() - { - this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); - - double minVariance = double.MaxValue; - double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out int quality); - - minVariance = Math.Min(minVariance, variance); - maxVariance = Math.Max(maxVariance, variance); - - this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); - } - - this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); - } } } From c64ac5f8ecaa63707f72eaabf8a9ba94032feb04 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 00:19:52 +0300 Subject: [PATCH 31/40] Comments, quantization table scaling with clamp call --- .../Formats/Jpeg/Components/Quantization.cs | 15 ++------------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 7 +++++++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 7e528d056d..394ad71afd 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -179,19 +179,8 @@ private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan un Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { - int x = unscaledTable[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - table[j] = x; + int x = ((unscaledTable[j] * scale) + 50) / 100; + table[j] = Numerics.Clamp(x, 1, 255); } return table; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 1b17bdce7b..0a4b970f4f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,7 +11,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Backing field for + /// private int? luminanceQuality; + + /// + /// Backing field for + /// private int? chrominanceQuality; /// From e205bea7025257b809b63bc31fb4a1166a164510 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 00:58:36 +0300 Subject: [PATCH 32/40] Tests --- .../Formats/Jpg/Block8x8FTests.cs | 91 +++++++++++++++++++ .../Formats/Jpg/JpegMetadataTests.cs | 39 ++++++++ 2 files changed, 130 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 4effc52b23..c68b0ffa85 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -493,5 +493,96 @@ public void LoadFromUInt16ExtendedAvx2() Assert.Equal(data[i], dest[i]); } } + + [Fact] + public void EqualsToScalar_AllOne() + { + static void RunTest() + { + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = 1; + } + + bool isEqual = block.EqualsToScalar(1); + Assert.True(isEqual); + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(10)] + public void EqualsToScalar_OneOffEachPosition(int equalsTo) + { + static void RunTest(string serializedEqualsTo) + { + int equalsTo = FeatureTestRunner.Deserialize(serializedEqualsTo); + int offValue = 0; + + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = equalsTo; + } + + // Assert with invalid values at different positions + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = offValue; + + bool isEqual = block.EqualsToScalar(equalsTo); + Assert.False(isEqual, $"False equality:\n{block}"); + + // restore valid value for next iteration assertion + block[i] = equalsTo; + } + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + equalsTo, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(39)] + public void EqualsToScalar_Valid(int equalsTo) + { + static void RunTest(string serializedEqualsTo) + { + int equalsTo = FeatureTestRunner.Deserialize(serializedEqualsTo); + + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = equalsTo; + } + + // Assert + bool isEqual = block.EqualsToScalar(equalsTo); + Assert.True(isEqual); + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + equalsTo, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs index 503ede1299..56bf207b97 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -21,5 +21,44 @@ public void CloneIsDeep() Assert.False(meta.Quality.Equals(clone.Quality)); Assert.False(meta.ColorType.Equals(clone.ColorType)); } + + [Fact] + public void Quality_DefaultQuality() + { + var meta = new JpegMetadata(); + + Assert.Equal(meta.Quality, ImageSharp.Formats.Jpeg.Components.Quantization.DefaultQualityFactor); + } + + [Fact] + public void Quality_LuminanceOnlyQuality() + { + int quality = 50; + + var meta = new JpegMetadata { LuminanceQuality = quality }; + + Assert.Equal(meta.Quality, quality); + } + + [Fact] + public void Quality_BothComponentsQuality() + { + int quality = 50; + + var meta = new JpegMetadata { LuminanceQuality = quality, ChrominanceQuality = quality }; + + Assert.Equal(meta.Quality, quality); + } + + [Fact] + public void Quality_ReturnsMaxQuality() + { + int qualityLuma = 50; + int qualityChroma = 30; + + var meta = new JpegMetadata { LuminanceQuality = qualityLuma, ChrominanceQuality = qualityChroma }; + + Assert.Equal(meta.Quality, qualityLuma); + } } } From 99b24fefd35d18bb47c2007c8f50268877eb15c3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:26:14 +0300 Subject: [PATCH 33/40] Added images to quality estimation tests --- .../ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs | 4 +++- tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Jpg/baseline/forest_bridge.jpg | 3 +++ tests/Images/Input/Jpg/progressive/winter.jpg | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Jpg/baseline/forest_bridge.jpg create mode 100644 tests/Images/Input/Jpg/progressive/winter.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 9d4aea453b..403eeaf908 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -55,7 +55,9 @@ public partial class JpegDecoderTests { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, - { TestImages.Jpeg.Issues.IncorrectQuality845, 98 } + { TestImages.Jpeg.Issues.IncorrectQuality845, 98 }, + { TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, 89 }, + { TestImages.Jpeg.Progressive.Winter, 80 } }; [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6d2f65f575..d7fd3b569f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -157,6 +157,7 @@ public static class Progressive public const string Fb = "Jpg/progressive/fb.jpg"; public const string Progress = "Jpg/progressive/progress.jpg"; public const string Festzug = "Jpg/progressive/Festzug.jpg"; + public const string Winter = "Jpg/progressive/winter.jpg"; public static class Bad { @@ -198,6 +199,7 @@ public static class Bad public const string Iptc = "Jpg/baseline/iptc.jpg"; public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg"; public const string HistogramEqImage = "Jpg/baseline/640px-Unequalized_Hawkes_Bay_NZ.jpg"; + public const string ForestBridgeDifferentComponentsQuality = "Jpg/baseline/forest_bridge.jpg"; public static readonly string[] All = { diff --git a/tests/Images/Input/Jpg/baseline/forest_bridge.jpg b/tests/Images/Input/Jpg/baseline/forest_bridge.jpg new file mode 100644 index 0000000000..a487bb9e7c --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/forest_bridge.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56b3db3d0e146ee7fe27f8fbda4bccc1483e18104bfc747cac75a2ec03d65647 +size 1936782 diff --git a/tests/Images/Input/Jpg/progressive/winter.jpg b/tests/Images/Input/Jpg/progressive/winter.jpg new file mode 100644 index 0000000000..bc08d8be00 --- /dev/null +++ b/tests/Images/Input/Jpg/progressive/winter.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d377b70cedfb9d25f1ae0244dcf2edb000540aa4a8925cce57f810f7efd0dc84 +size 234976 From 906ac84d42e9298982cc0d90205c8e64e7730bd3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:38:13 +0300 Subject: [PATCH 34/40] Fixed invalid chrominance quality estimation --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 394ad71afd..74fb19f547 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -166,7 +166,7 @@ public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) - => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance); + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) From 7463b49e7b9bfb5d74af511610dbfc9e893d13ef Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:43:06 +0300 Subject: [PATCH 35/40] Used nint in Unsafe.Add calls --- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index d55dfced72..4cf8be44fd 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ public bool EqualsToScalar(int value) var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (int i = 0; i < RowCount; i++) + for (nint i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ public bool EqualsToScalar(int value) { ref float scalars = ref Unsafe.As(ref this); - for (int i = 0; i < Size; i++) + for (nint i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From eb6888a3787b7d2f92f31125b4e244527bf5514e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:47:12 +0300 Subject: [PATCH 36/40] Fixed warnings --- .../Formats/Jpeg/Components/Quantization.cs | 11 +++-------- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 2 -- .../ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs | 2 -- .../ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs | 1 - 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 74fb19f547..8e5f928b00 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { @@ -87,8 +85,7 @@ internal static class Quantization /// /// Input quantization table. /// Quantization to estimate against. - /// Estimated quality - /// indicating if given table is target-complient + /// Estimated quality public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) { // This method can be SIMD'ified if standard table is injected as Block8x8F. @@ -152,8 +149,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Luminance quantization table. - /// Output jpeg quality. - /// indicating if given table is ITU-complient. + /// Estimated quality [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); @@ -162,8 +158,7 @@ public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Chrominance quantization table. - /// Output jpeg quality. - /// indicating if given table is ITU-complient. + /// Estimated quality [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance); diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 39b8e492f8..b0bdbf0ed2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -4,8 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 2f673ef2f9..03f7020c09 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -1,10 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.Formats.Jpeg.Components; using Xunit; -using Xunit.Abstractions; using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 0b819bf13c..40b9e68677 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; From 8b12623f7bcace057b5340d3848a58416d7a3c3e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:50:04 +0300 Subject: [PATCH 37/40] Revert "Used nint in Unsafe.Add calls" This reverts commit 7463b49e7b9bfb5d74af511610dbfc9e893d13ef. --- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 4cf8be44fd..d55dfced72 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ public bool EqualsToScalar(int value) var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (nint i = 0; i < RowCount; i++) + for (int i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ public bool EqualsToScalar(int value) { ref float scalars = ref Unsafe.As(ref this); - for (nint i = 0; i < Size; i++) + for (int i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From 84e52e767aafbb07d3222c6764a70ecd762a0c61 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 03:10:02 +0300 Subject: [PATCH 38/40] Moved quality debug check to a proper place --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 8e5f928b00..2ff56c63b9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -165,12 +165,14 @@ public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) - => quality < 50 ? 5000 / quality : 200 - (quality * 2); + { + DebugGuard.MustBeBetweenOrEqualTo(quality, MinQualityFactor, MaxQualityFactor, nameof(quality)); + + return quality < 50 ? (5000 / quality) : (200 - (quality * 2)); + } private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) { - DebugGuard.MustBeBetweenOrEqualTo(scale, MinQualityFactor, MaxQualityFactor, nameof(scale)); - Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { From eaf40fc63918d30ce63e08866f6fc07c346557d3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 03:13:18 +0300 Subject: [PATCH 39/40] nint usage --- shared-infrastructure | 2 +- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-infrastructure b/shared-infrastructure index 9b94ebc4be..847a4e4c84 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 9b94ebc4be9b7a8d7620c257e6ee485455973332 +Subproject commit 847a4e4c8443fabafdd5c0a5fcf5fc3a32ab1f73 diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index d55dfced72..4cf8be44fd 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ public bool EqualsToScalar(int value) var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (int i = 0; i < RowCount; i++) + for (nint i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ public bool EqualsToScalar(int value) { ref float scalars = ref Unsafe.As(ref this); - for (int i = 0; i < Size; i++) + for (nint i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From 66604f40cdb04d5db86f6283a8ae31e2d7332b8a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 04:47:44 +0300 Subject: [PATCH 40/40] Revert "nint usage" This reverts commit eaf40fc63918d30ce63e08866f6fc07c346557d3. --- shared-infrastructure | 2 +- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-infrastructure b/shared-infrastructure index 847a4e4c84..9b94ebc4be 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 847a4e4c8443fabafdd5c0a5fcf5fc3a32ab1f73 +Subproject commit 9b94ebc4be9b7a8d7620c257e6ee485455973332 diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 4cf8be44fd..d55dfced72 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ public bool EqualsToScalar(int value) var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (nint i = 0; i < RowCount; i++) + for (int i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ public bool EqualsToScalar(int value) { ref float scalars = ref Unsafe.As(ref this); - for (nint i = 0; i < Size; i++) + for (int i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) {