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/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index ba3dfb6296..49ac494796 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/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs deleted file mode 100644 index 938459b88e..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ /dev/null @@ -1,144 +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 an estimated quality of the image based on the quantization tables. - /// - /// The quantization tables. - /// The . - public static int EstimateQuality(Block8x8F[] quantizationTables) - { - int 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; - } - - if (((quality <= Hash[i]) && (sum <= Sums[i])) || (i >= 50)) - { - return i + 1; - } - } - } - else - { - quality = (int)(qTable0[2] + qTable0[53]); - - for (int i = 0; i < 100; i++) - { - if (quality < Hash1[i] && sum < Sums1[i]) - { - continue; - } - - if (((quality <= Hash1[i]) && (sum <= Sums1[i])) || (i >= 50)) - { - return i + 1; - } - } - } - } - - return quality; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs new file mode 100644 index 0000000000..2ff56c63b9 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -0,0 +1,194 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Provides methods and properties related to jpeg quantization. + /// + 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; + + /// + /// 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 + /// which shouldn't use standard itu quantization tables for re-encoding. + /// + public const int QualityEstimationConfidenceLowerThreshold = 25; + + /// + /// Represents highest quality setting which can be estimated with enough confidence. + /// + public const int QualityEstimationConfidenceUpperThreshold = 98; + + /// + /// 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, + }; + + /// Ported from JPEGsnoop: + /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + /// + /// 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: + /// and . + /// + /// Input quantization table. + /// Quantization to estimate against. + /// Estimated 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; + + // 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' which will affect result filesize drastically. + // Quality=100 shouldn't be used in usual use case. + return 100; + } + + int quality; + 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 + { + // 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; + } + + // 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; + + // Generate the equivalent IJQ "quality" factor + if (sumPercent <= 100.0) + { + quality = (int)Math.Round((200 - sumPercent) / 2); + } + else + { + quality = (int)Math.Round(5000.0 / sumPercent); + } + + return quality; + } + + /// + /// Estimates jpeg quality based on quantization table in zig-zag order. + /// + /// Luminance quantization table. + /// Estimated quality + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); + + /// + /// Estimates jpeg quality based on quantization table in zig-zag order. + /// + /// Chrominance quantization table. + /// Estimated quality + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int QualityToScale(int quality) + { + DebugGuard.MustBeBetweenOrEqualTo(quality, MinQualityFactor, MaxQualityFactor, nameof(quality)); + + return 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] * scale) + 50) / 100; + table[j] = Numerics.Clamp(x, 1, 255); + } + + 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/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index cceed407c2..a9f564b450 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -9,11 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal interface IJpegEncoderOptions { /// - /// Gets the quality, that will be used to encode the image. Quality + /// 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? Quality { 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/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/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 77b1b44aff..896e5f0aaf 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -699,81 +699,95 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining) /// private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { + JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + 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--; + + // Decoding single 8x8 table + ref Block8x8F table = ref this.QuantizationTables[tableIndex]; + 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); remaining -= 64; - ref Block8x8F table = ref this.QuantizationTables[tableIndex]; for (int j = 0; j < 64; j++) { 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); 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]; } - } - break; + break; + } + // Unknown precision - error default: { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTablePrecision(tablePrecision); break; } } - if (done) + // Estimating quality + switch (tableIndex) { - break; - } - } + // luminance table + case 0: + { + jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref table); + break; + } - if (remaining != 0) - { - JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); + // chrominance table + case 1: + { + jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref table); + break; + } + } } - - this.Metadata.GetFormatMetadata(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 8131f74d26..5e199b4204 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -13,11 +13,7 @@ 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 { get; set; } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 135048aa4e..88d96f554d 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. /// @@ -124,35 +86,14 @@ 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; - - // Convert from a quality rating to a scaling factor. - int scale; - if (qlty < 50) - { - scale = 5000 / qlty; - } - else - { - scale = 200 - (qlty * 2); - } - + // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that // 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); - } + this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -176,10 +117,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: @@ -690,31 +633,49 @@ private void WriteMarkerHeader(byte marker, int length) } /// - /// Initializes quantization table. + /// Initializes quntization tables. /// - /// The quantization index. - /// The scaling factor. - /// The quantization table. - private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) + /// + /// 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) { - DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); - ReadOnlySpan unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance; + int lumaQuality; + int chromaQuality; + if (this.quality.HasValue) + { + lumaQuality = this.quality.Value; + chromaQuality = this.quality.Value; + } + else + { + lumaQuality = metadata.LuminanceQuality; + chromaQuality = metadata.ChrominanceQuality; + } - for (int j = 0; j < Block8x8F.Size; j++) + // Luminance + lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + + // Chrominance + chrominanceQuantTable = default; + if (componentCount > 1) { - int x = unscaledQuant[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } + chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); - if (x > 255) + if (!this.subsample.HasValue) { - x = 255; + this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; } - - quant[j] = x; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 9670d167e0..0a4b970f4f 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,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Backing field for + /// + private int? luminanceQuality; + + /// + /// Backing field for + /// + private int? chrominanceQuality; + /// /// Initializes a new instance of the class. /// @@ -21,18 +34,80 @@ public JpegMetadata() /// The metadata to create an instance from. private JpegMetadata(JpegMetadata other) { - this.Quality = other.Quality; this.ColorType = other.ColorType; + + this.luminanceQuality = other.luminanceQuality; + this.chrominanceQuality = other.chrominanceQuality; } /// - /// Gets or sets the encoded quality. + /// 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. + /// + internal int LuminanceQuality + { + get => this.luminanceQuality ?? Quantization.DefaultQualityFactor; + set => this.luminanceQuality = value; + } + + /// + /// Gets or sets the jpeg chrominance quality. /// - public int Quality { get; set; } = 75; + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-complient ITU quantization tables. + /// + internal int ChrominanceQuality + { + get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor; + set => this.chrominanceQuality = value; + } /// /// Gets or sets the encoded quality. /// + /// + /// 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 + { + get + { + // Jpeg always has a luminance table thus it must have a luminance quality derived from it + if (!this.luminanceQuality.HasValue) + { + return Quantization.DefaultQualityFactor; + } + + int lumaQuality = this.luminanceQuality.Value; + + // Jpeg might not have a chrominance table - return luminance quality (grayscale images) + if (!this.chrominanceQuality.HasValue) + { + 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(lumaQuality, chromaQuality); + } + + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } + } + + /// + /// Gets or sets the color type. + /// public JpegColorType? ColorType { get; set; } /// 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."); 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/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index f47ae55220..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, 99 } + { TestImages.Jpeg.Issues.IncorrectQuality845, 98 }, + { TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, 89 }, + { TestImages.Jpeg.Progressive.Winter, 80 } }; [Theory] 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); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs new file mode 100644 index 0000000000..03f7020c09 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using Xunit; + +using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class QuantizationTests + { + [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); + int estimatedQuality = JpegQuantization.EstimateLuminanceQuality(ref table); + + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate luminance quality for standard table at quality level {quality}"); + } + } + + [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); + int estimatedQuality = JpegQuantization.EstimateChrominanceQuality(ref table); + + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate chrominance quality for standard table at quality level {quality}"); + } + } + } +} 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; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9c24184366..2d12e5f71a 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