diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
index b87c538a9..3087551d0 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
///
/// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
// 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
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 000000000..1870c39fa
--- /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}");
+ }
+ }
+}