diff --git a/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs new file mode 100644 index 000000000..b6a628608 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Common.Helpers +{ + internal readonly struct ExifResolutionValues + { + public ExifResolutionValues(ushort resolutionUnit, double? horizontalResolution, double? verticalResolution) + { + this.ResolutionUnit = resolutionUnit; + this.HorizontalResolution = horizontalResolution; + this.VerticalResolution = verticalResolution; + } + + public ushort ResolutionUnit { get; } + + public double? HorizontalResolution { get; } + + public double? VerticalResolution { get; } + } +} diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index efc0e0e15..7ea64aa62 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -98,14 +98,14 @@ namespace SixLabors.ImageSharp.Common.Helpers } /// - /// Sets the exif profile resolution values. + /// Gets the exif profile resolution values. /// - /// The exif profile. /// The resolution unit. /// The horizontal resolution value. /// The vertical resolution value. + /// [MethodImpl(InliningOptions.ShortMethod)] - public static void SetResolutionValues(ExifProfile exifProfile, PixelResolutionUnit unit, double horizontal, double vertical) + public static ExifResolutionValues GetExifResolutionValues(PixelResolutionUnit unit, double horizontal, double vertical) { switch (unit) { @@ -115,9 +115,9 @@ namespace SixLabors.ImageSharp.Common.Helpers break; case PixelResolutionUnit.PixelsPerMeter: { - unit = PixelResolutionUnit.PixelsPerCentimeter; - horizontal = UnitConverter.MeterToCm(horizontal); - vertical = UnitConverter.MeterToCm(vertical); + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = MeterToCm(horizontal); + vertical = MeterToCm(vertical); } break; @@ -126,18 +126,13 @@ namespace SixLabors.ImageSharp.Common.Helpers break; } - exifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1)); - + ushort exifUnit = (ushort)(unit + 1); if (unit == PixelResolutionUnit.AspectRatio) { - exifProfile.RemoveValue(ExifTag.XResolution); - exifProfile.RemoveValue(ExifTag.YResolution); - } - else - { - exifProfile.SetValue(ExifTag.XResolution, new Rational(horizontal)); - exifProfile.SetValue(ExifTag.YResolution, new Rational(vertical)); + return new ExifResolutionValues(exifUnit, null, null); } + + return new ExifResolutionValues(exifUnit, horizontal, vertical); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 8ca7b0c80..d55dfced7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -830,5 +830,46 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components 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 ba3dfb629..49ac49479 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder 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 938459b88..000000000 --- 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 000000000..2ff56c63b --- /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 cceed407c..a9f564b45 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 39b8e492f..b0bdbf0ed 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 77b1b44af..896e5f0aa 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -699,81 +699,95 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// 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 8131f74d2..5e199b420 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 135048aa4..88d96f554 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -64,44 +64,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// 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 9670d167e..0a4b970f4 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 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// 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 cc75870e1..1b5362275 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -36,7 +36,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs new file mode 100644 index 000000000..459506843 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 16-bit grayscale images. + /// + internal class BlackIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero16TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + L16 l16 = TiffUtils.L16Default; + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); + } + } + else + { + int byteCount = pixelRow.Length * 2; + PixelOperations.Instance.FromL16Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs new file mode 100644 index 000000000..ec07abd5c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 24-bit grayscale images. + /// + internal class BlackIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer.AsSpan(bufferStartIdx); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs new file mode 100644 index 000000000..862756bc4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 32-bit grayscale images. + /// + internal class BlackIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs index 096f0449b..f62cf2952 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -14,25 +13,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class BlackIsZero8TiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly Configuration configuration; + + public BlackIsZero8TiffColor(Configuration configuration) => this.configuration = configuration; + /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { - var color = default(TPixel); - int offset = 0; - var l8 = default(L8); for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) - { - byte intensity = data[offset++]; - - l8.PackedValue = intensity; - color.FromL8(l8); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length; + PixelOperations.Instance.FromL8Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); - pixels[x, y] = color; - } + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs index a4e5e45df..9956db523 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = value / this.factor; color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index 796227953..b392fe1a3 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -35,10 +35,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int index = bitReader.ReadBits(this.bitsPerSample0); - pixels[x, y] = this.palette[index]; + pixelRow[x] = this.palette[index]; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 635be95f4..e5d8c8da2 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -16,43 +16,60 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly bool isBigEndian; + private readonly Configuration configuration; + /// /// Initializes a new instance of the class. /// + /// The configuration. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public Rgb161616TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + public Rgb161616TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); int offset = 0; - var rgba = default(Rgba64); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); - for (int x = left; x < left + width; x++) + if (this.isBigEndian) { - ulong r = this.ConvertToShort(data.Slice(offset, 2)); - offset += 2; - ulong g = this.ConvertToShort(data.Slice(offset, 2)); - offset += 2; - ulong b = this.ConvertToShort(data.Slice(offset, 2)); - offset += 2; + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong g = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong b = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; - rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); - color.FromRgba64(rgba); + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } + } + else + { + int byteCount = pixelRow.Length * 6; + PixelOperations.Instance.FromRgb48Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); - pixelRow[x] = color; + offset += byteCount; } } } - - private ushort ConvertToShort(ReadOnlySpan buffer) => this.isBigEndian - ? BinaryPrimitives.ReadUInt16BigEndian(buffer) - : BinaryPrimitives.ReadUInt16LittleEndian(buffer); } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..9a6d4631a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 16 bit. + /// + internal class Rgb16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb16PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortBigEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortBigEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortBigEndian(blueData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortLittleEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortLittleEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortLittleEndian(blueData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs new file mode 100644 index 000000000..3be0540a0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 24 bits for each channel. + /// + internal class Rgb242424TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb242424TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + int offset = 0; + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer.AsSpan(bufferStartIdx); + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..9c3e57e2a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 24 bit. + /// + internal class Rgb24PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb24PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span bufferSpan = buffer.AsSpan(bufferStartIdx); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs new file mode 100644 index 000000000..e2ba085e1 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 32 bits for each channel. + /// + internal class Rgb323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..a7432549c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 32 bit. + /// + internal class Rgb32PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb32PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntBigEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntBigEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntLittleEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntLittleEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs index e45863a57..2a86eb2ee 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -14,29 +13,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class Rgb888TiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly Configuration configuration; + + public Rgb888TiffColor(Configuration configuration) => this.configuration = configuration; + /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { - var color = default(TPixel); - int offset = 0; - var rgba = default(Rgba32); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - - for (int x = left; x < left + width; x++) - { - byte r = data[offset++]; - byte g = data[offset++]; - byte b = data[offset++]; - - rgba.PackedValue = (uint)(r | (g << 8) | (b << 16) | (0xff << 24)); - color.FromRgba32(rgba); - - pixelRow[x] = color; - } + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length * 3; + PixelOperations.Instance.FromRgb24Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index 3400bd65d..b442c4ae4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; @@ -12,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// /// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths). /// - internal class RgbPlanarTiffColor + internal class RgbPlanarTiffColor : TiffBasePlanarColorDecoder where TPixel : unmanaged, IPixel { private readonly float rFactor; @@ -47,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// The y-coordinate of the top of the image block. /// The width of the image block. /// The height of the image block. - public void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) { var color = default(TPixel); @@ -57,14 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; color.FromVector4(new Vector4(r, g, b, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } rBitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 259bb8efa..1377598cc 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -47,14 +47,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; color.FromVector4(new Vector4(r, g, b, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs new file mode 100644 index 000000000..57d8588ce --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// The base class for planar color decoders. + /// + /// The pixel format. + internal abstract class TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + /// + /// Decodes source raw pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public abstract void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 8e711d3eb..4a2fe93fe 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal static class TiffColorDecoderFactory where TPixel : unmanaged, IPixel { - public static TiffBaseColorDecoder Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBaseColorDecoder Create(Configuration configuration, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) { switch (colorType) { @@ -32,6 +32,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new WhiteIsZero8TiffColor(); + case TiffColorType.WhiteIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.WhiteIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.WhiteIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero: DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); @@ -50,7 +65,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation case TiffColorType.BlackIsZero8: DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new BlackIsZero8TiffColor(); + return new BlackIsZero8TiffColor(configuration); + + case TiffColorType.BlackIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero16TiffColor(configuration, byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.BlackIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); @@ -84,7 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation && bitsPerSample.Channel0 == 8, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new Rgb888TiffColor(); + return new Rgb888TiffColor(configuration); case TiffColorType.Rgb101010: DebugGuard.IsTrue( @@ -124,7 +154,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation && bitsPerSample.Channel0 == 16, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new Rgb161616TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + return new Rgb161616TiffColor(configuration, isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb242424: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 24 + && bitsPerSample.Channel1 == 24 + && bitsPerSample.Channel0 == 24, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb242424TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); @@ -135,14 +185,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } - public static RgbPlanarTiffColor CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap) + public static TiffBasePlanarColorDecoder CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) { switch (colorType) { - case TiffColorType.RgbPlanar: + case TiffColorType.Rgb888Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.Rgb161616Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb242424Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb323232Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb32PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 517926c23..dc47dc8cd 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -28,6 +28,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// BlackIsZero8, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 16-bit images. + /// + BlackIsZero16, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 24-bit images. + /// + BlackIsZero24, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 32-bit images. + /// + BlackIsZero32, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. /// @@ -48,6 +63,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// WhiteIsZero8, + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 16-bit images. + /// + WhiteIsZero16, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 24-bit images. + /// + WhiteIsZero24, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 32-bit images. + /// + WhiteIsZero32, + /// /// Palette-color. /// @@ -94,8 +124,33 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Rgb161616, /// - /// RGB Full Color. Planar configuration of data. + /// RGB color image with 24 bits for each channel. + /// + Rgb242424, + + /// + /// RGB color image with 32 bits for each channel. + /// + Rgb323232, + + /// + /// RGB Full Color. Planar configuration of data. 8 Bit per color channel. + /// + Rgb888Planar, + + /// + /// RGB Full Color. Planar configuration of data. 16 Bit per color channel. + /// + Rgb161616Planar, + + /// + /// RGB Full Color. Planar configuration of data. 24 Bit per color channel. + /// + Rgb242424Planar, + + /// + /// RGB Full Color. Planar configuration of data. 32 Bit per color channel. /// - RgbPlanar, + Rgb323232Planar, } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs new file mode 100644 index 000000000..18b5300b2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 16-bit grayscale images. + /// + internal class WhiteIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero16TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + L16 l16 = TiffUtils.L16Default; + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2))); + offset += 2; + + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2))); + offset += 2; + + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs new file mode 100644 index 000000000..b1088732b --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 24-bit grayscale images. + /// + internal class WhiteIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer.AsSpan(bufferStartIdx); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs new file mode 100644 index 000000000..007174003 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 32-bit grayscale images. + /// + internal class WhiteIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs index 1b141f9f6..6a6c2af22 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; - +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -24,14 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l8 = default(L8); for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { byte intensity = (byte)(255 - data[offset++]); - - l8.PackedValue = intensity; - color.FromL8(l8); - - pixels[x, y] = color; + pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs index 04b6f98e5..912955964 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = 1.0f - (value / this.factor); color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 484e182c5..63185619d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -266,17 +266,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); - RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap); + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + int stripIndex = i; for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) { - int stripIndex = (i * stripsPerPixel) + planeIndex; - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); + stripIndex += stripsPerPlane; } colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); @@ -316,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.Predictor, this.FaxCompressionOptions); - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.Configuration, this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 288f01cd1..8590203a7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -104,13 +104,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 16) + if (bitsPerChannel > 32) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 32: + { + options.ColorType = TiffColorType.WhiteIsZero32; + break; + } + + case 24: + { + options.ColorType = TiffColorType.WhiteIsZero24; + break; + } + + case 16: + { + options.ColorType = TiffColorType.WhiteIsZero16; + break; + } + case 8: { options.ColorType = TiffColorType.WhiteIsZero8; @@ -147,13 +165,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 16) + if (bitsPerChannel > 32) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 32: + { + options.ColorType = TiffColorType.BlackIsZero32; + break; + } + + case 24: + { + options.ColorType = TiffColorType.BlackIsZero24; + break; + } + + case 16: + { + options.ColorType = TiffColorType.BlackIsZero16; + break; + } + case 8: { options.ColorType = TiffColorType.BlackIsZero8; @@ -184,16 +220,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffPhotometricInterpretation.Rgb: { - if (options.BitsPerSample.Channels != 3) + TiffBitsPerSample bitsPerSample = options.BitsPerSample; + if (bitsPerSample.Channels != 3) { TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } + if (!(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2)) + { + TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); + } + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { ushort bitsPerChannel = options.BitsPerSample.Channel0; switch (bitsPerChannel) { + case 32: + options.ColorType = TiffColorType.Rgb323232; + break; + + case 24: + options.ColorType = TiffColorType.Rgb242424; + break; + case 16: options.ColorType = TiffColorType.Rgb161616; break; @@ -226,7 +276,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff } else { - options.ColorType = TiffColorType.RgbPlanar; + ushort bitsPerChannel = options.BitsPerSample.Channel0; + switch (bitsPerChannel) + { + case 32: + options.ColorType = TiffColorType.Rgb323232Planar; + break; + case 24: + options.ColorType = TiffColorType.Rgb242424Planar; + break; + case 16: + options.ColorType = TiffColorType.Rgb161616Planar; + break; + default: + options.ColorType = TiffColorType.Rgb888Planar; + break; + } } break; diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 2273d759f..d7c9848a4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -74,6 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + private readonly List<(long, uint)> frameMarkers = new List<(long, uint)>(); + /// /// Initializes a new instance of the class. /// @@ -147,13 +149,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff // Make sure, the Encoder options makes sense in combination with each other. this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); - using (var writer = new TiffStreamWriter(stream)) + using var writer = new TiffStreamWriter(stream); + long ifdMarker = this.WriteHeader(writer); + + Image metadataImage = image; + foreach (ImageFrame frame in image.Frames) { - long firstIfdMarker = this.WriteHeader(writer); + var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); - // TODO: multiframing is not supported - this.WriteImage(writer, image, firstIfdMarker); + ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); + metadataImage = null; + } + + long currentOffset = writer.BaseStream.Position; + foreach ((long, uint) marker in this.frameMarkers) + { + writer.WriteMarkerFast(marker.Item1, marker.Item2); } + + writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// @@ -174,41 +188,56 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Writes all data required to define an image. /// /// The pixel format. - /// The to write data to. - /// The to encode from. + /// The to write data to. + /// The tiff frame. + /// The image metadata (resolution values for each frame). + /// The image (common metadata for root frame). /// The marker to write this IFD offset. - private void WriteImage(TiffStreamWriter writer, Image image, long ifdOffset) + /// + /// The next IFD offset value. + /// + private long WriteFrame( + TiffStreamWriter writer, + ImageFrame frame, + ImageMetadata imageMetadata, + Image image, + long ifdOffset) where TPixel : unmanaged, IPixel { - var entriesCollector = new TiffEncoderEntriesCollector(); - using TiffBaseCompressor compressor = TiffCompressorFactory.Create( this.CompressionType ?? TiffCompression.None, writer.BaseStream, this.memoryAllocator, - image.Width, + frame.Width, (int)this.BitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + var entriesCollector = new TiffEncoderEntriesCollector(); using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, - image.Frames.RootFrame, + frame, this.quantizer, this.memoryAllocator, this.configuration, entriesCollector, (int)this.BitsPerPixel); - int rowsPerStrip = this.CalcRowsPerStrip(image.Frames.RootFrame.Height, colorWriter.BytesPerRow); + int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow); colorWriter.Write(compressor, rowsPerStrip); + if (image != null) + { + entriesCollector.ProcessMetadata(image); + } + + entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessImageFormat(this); - entriesCollector.ProcessGeneral(image); - writer.WriteMarker(ifdOffset, (uint)writer.Position); - long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); + this.frameMarkers.Add((ifdOffset, (uint)writer.Position)); + + return this.WriteIfd(writer, entriesCollector.Entries); } /// @@ -272,7 +301,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } else { - var raw = new byte[length]; + byte[] raw = new byte[length]; int sz = ExifWriter.WriteValue(entry, raw, 0); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); largeDataBlocks.Add(raw); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 43a086849..15694978f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,7 +6,6 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff { @@ -16,9 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff public List Entries { get; } = new List(); - public void ProcessGeneral(Image image) - where TPixel : unmanaged, IPixel - => new GeneralProcessor(this).Process(image); + public void ProcessMetadata(Image image) + => new MetadataProcessor(this).Process(image); + + public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, imageMetadata); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -38,44 +39,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void Add(IExifValue entry) => this.Entries.Add(entry); - private class GeneralProcessor + private abstract class BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; - public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + protected TiffEncoderEntriesCollector Collector { get; } + } - public void Process(Image image) - where TPixel : unmanaged, IPixel + private class MetadataProcessor : BaseProcessor + { + public MetadataProcessor(TiffEncoderEntriesCollector collector) + : base(collector) { - ImageFrame rootFrame = image.Frames.RootFrame; - ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); - byte[] rootFrameXmpBytes = rootFrame.Metadata.XmpProfile; - - var width = new ExifLong(ExifTagValue.ImageWidth) - { - Value = (uint)image.Width - }; - - var height = new ExifLong(ExifTagValue.ImageLength) - { - Value = (uint)image.Height - }; - - var software = new ExifString(ExifTagValue.Software) - { - Value = SoftwareValue - }; + } - this.collector.AddOrReplace(width); - this.collector.AddOrReplace(height); + public void Process(Image image) + { + ImageFrame rootFrame = image.Frames.RootFrame; + ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); + byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile; - this.ProcessResolution(image.Metadata, rootFrameExifProfile); - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes); this.ProcessMetadata(rootFrameExifProfile); - if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) { - this.collector.Add(software); + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); } } @@ -114,26 +106,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void ProcessResolution(ImageMetadata imageMetadata, ExifProfile exifProfile) - { - UnitConverter.SetResolutionValues( - exifProfile, - imageMetadata.ResolutionUnits, - imageMetadata.HorizontalResolution, - imageMetadata.VerticalResolution); - - this.collector.Add(exifProfile.GetValue(ExifTag.ResolutionUnit).DeepClone()); - - IExifValue xResolution = exifProfile.GetValue(ExifTag.XResolution)?.DeepClone(); - IExifValue yResolution = exifProfile.GetValue(ExifTag.YResolution)?.DeepClone(); - - if (xResolution != null && yResolution != null) - { - this.collector.Add(xResolution); - this.collector.Add(yResolution); - } - } - private void ProcessMetadata(ExifProfile exifProfile) { foreach (IExifValue entry in exifProfile.Values) @@ -170,9 +142,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag)) + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag)) { - this.collector.AddOrReplace(entry.DeepClone()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -183,12 +155,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff { foreach (IExifValue entry in exifProfile.Values) { - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) { ExifParts entryPart = ExifTags.GetPart(entry.Tag); if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart)) { - this.collector.AddOrReplace(entry.DeepClone()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -206,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IptcProfile.Data }; - this.collector.Add(iptc); + this.Collector.Add(iptc); } else { @@ -220,7 +192,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IccProfile.ToByteArray() }; - this.collector.Add(icc); + this.Collector.Add(icc); } else { @@ -234,7 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = xmpProfile }; - this.collector.Add(xmp); + this.Collector.Add(xmp); } else { @@ -243,11 +215,61 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private class ImageFormatProcessor + private class FrameInfoProcessor : BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public FrameInfoProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(ImageFrame frame, ImageMetadata imageMetadata) + { + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)frame.Width + }); + + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)frame.Height + }); + + this.ProcessResolution(imageMetadata); + } + + private void ProcessResolution(ImageMetadata imageMetadata) + { + ExifResolutionValues resolution = UnitConverter.GetExifResolutionValues( + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); - public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + this.Collector.AddOrReplace(new ExifShort(ExifTagValue.ResolutionUnit) + { + Value = resolution.ResolutionUnit + }); + + if (resolution.VerticalResolution.HasValue && resolution.HorizontalResolution.HasValue) + { + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.XResolution) + { + Value = new Rational(resolution.HorizontalResolution.Value) + }); + + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.YResolution) + { + Value = new Rational(resolution.VerticalResolution.Value) + }); + } + } + } + + private class ImageFormatProcessor : BaseProcessor + { + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } public void Process(TiffEncoderCore encoder) { @@ -278,11 +300,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = (ushort)encoder.PhotometricInterpretation }; - this.collector.AddOrReplace(planarConfig); - this.collector.AddOrReplace(samplesPerPixel); - this.collector.AddOrReplace(bitPerSample); - this.collector.AddOrReplace(compression); - this.collector.AddOrReplace(photometricInterpretation); + this.Collector.AddOrReplace(planarConfig); + this.Collector.AddOrReplace(samplesPerPixel); + this.Collector.AddOrReplace(bitPerSample); + this.Collector.AddOrReplace(compression); + this.Collector.AddOrReplace(photometricInterpretation); if (encoder.HorizontalPredictor == TiffPredictor.Horizontal) { @@ -292,7 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; - this.collector.AddOrReplace(predictor); + this.Collector.AddOrReplace(predictor); } } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs new file mode 100644 index 000000000..f2872858c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Utils +{ + /// + /// Helper methods for TIFF decoding. + /// + internal static class TiffUtils + { + private const float Scale24Bit = 1.0f / 0xFFFFFF; + + private const float Scale32Bit = 1.0f / 0xFFFFFFFF; + + public static Vector4 Vector4Default { get; } = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + + public static Rgba64 Rgba64Default { get; } = new Rgba64(0, 0, 0, 0); + + public static L16 L16Default { get; } = new L16(0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToUShortBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToUShortLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromL8(L8 l8, byte intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + l8.PackedValue = intensity; + color.FromL8(l8); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); + color.FromRgba64(rgba); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(r * Scale24Bit, g * Scale24Bit, b * Scale24Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(r * Scale32Bit, g * Scale32Bit, b * Scale32Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + l16.PackedValue = intensity; + color.FromL16(l16); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(ulong intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(intensity * Scale24Bit, intensity * Scale24Bit, intensity * Scale24Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(ulong intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(intensity * Scale32Bit, intensity * Scale32Bit, intensity * Scale32Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs index 05a1ca7a2..138274d3f 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -126,10 +126,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) { - long currentOffset = this.BaseStream.Position; + long back = this.BaseStream.Position; + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value); + this.BaseStream.Seek(back, SeekOrigin.Begin); + } + + public void WriteMarkerFast(long offset, uint value) + { this.BaseStream.Seek(offset, SeekOrigin.Begin); this.Write(value); - this.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 1a107c2cf..0fe2d4b2c 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -27,6 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public ErrorDither(in DenseMatrix matrix, int offset) { + Guard.MustBeGreaterThan(offset, 0, nameof(offset)); + this.matrix = matrix; this.offset = offset; } @@ -95,6 +97,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; @@ -122,6 +129,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + float scale = processor.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { @@ -210,5 +222,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// public override int GetHashCode() => HashCode.Combine(this.offset, this.matrix); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index c317ddf02..2f5a5cf85 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -24,6 +24,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public OrderedDither(uint length) { + Guard.MustBeGreaterThan(length, 0, nameof(length)); + DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); // Create a new matrix to run against, that pre-thresholds the values. @@ -109,6 +111,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int spread = CalculatePaletteSpread(destination.Palette.Length); float scale = quantizer.Options.DitherScale; @@ -134,6 +141,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int spread = CalculatePaletteSpread(processor.Palette.Length); float scale = processor.DitherScale; @@ -201,5 +213,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public override int GetHashCode() => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 861697594..c1b695f65 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -11,14 +11,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class OctreeQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class /// using the default . /// public OctreeQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 4f73f4ac8..5da674cc9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,7 +11,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); private readonly ReadOnlyMemory colorPalette; /// @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, DefaultOptions) + : this(palette, new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index 5dda17dc6..e717152f9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Processing.Processors.Dithering; - namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -10,13 +8,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WebSafePaletteQuantizer : PaletteQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 6675263df..8a96f8ecc 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -9,13 +9,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WernerPaletteQuantizer : PaletteQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 95adb7e5d..337948bef 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -10,14 +10,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WuQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class /// using the default . /// public WuQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 4effc52b2..c68b0ffa8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -493,5 +493,96 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg 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 f47ae5522..403eeaf90 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -55,7 +55,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { { 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 503ede129..56bf207b9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -21,5 +21,44 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg 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 000000000..03f7020c0 --- /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 0b819bf13..40b9e6867 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/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs index 769ab850e..38611c6f3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs @@ -188,7 +188,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation { AssertDecode(expectedResult, pixels => { - new BlackIsZero8TiffColor().Decode(inputData, pixels, left, top, width, height); + new BlackIsZero8TiffColor(Configuration.Default).Decode(inputData, pixels, left, top, width, height); }); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs index 9adf59e48..f9f633106 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation { AssertDecode(expectedResult, pixels => { - new Rgb888TiffColor().Decode(inputData, pixels, left, top, width, height); + new Rgb888TiffColor(Configuration.Default).Decode(inputData, pixels, left, top, width, height); }); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 97c19f291..47a3584e0 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -82,6 +82,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_Uncompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb888Planar6Strips, PixelTypes.Rgba32)] + [WithFile(FlowerRgb888Planar15Strips, PixelTypes.Rgba32)] + public void TiffDecoder_Planar(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] [WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)] @@ -105,49 +111,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower2BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_2Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_2Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(FlowerRgb222Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb222Planar, PixelTypes.Rgba32)] - [WithFile(Flower6BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_6Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower6BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_6Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower8BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_8Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_8Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(Flower10BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_10Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_10Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(FlowerRgb444Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)] - [WithFile(Flower12BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_12Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower12BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_12Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower14BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_14Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_14Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] + [WithFile(Flower16BitGrayLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) + [WithFile(Flower16BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] + [WithFile(Flower16BitGrayMinIsWhiteBigEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_16Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower24BitGray, PixelTypes.Rgba32)] + [WithFile(Flower24BitGrayLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_30Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower32BitGray, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayLittleEndian, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayMinIsWhite, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_32Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(FlowerRgb121212Contiguous, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_36Bit(TestImageProvider provider) @@ -161,11 +202,39 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb161616ContiguousLittleEndian, PixelTypes.Rgba32)] [WithFile(FlowerRgb161616Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb161616PlanarLittleEndian, PixelTypes.Rgba32)] [WithFile(Issues1716Rgb161616BitLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_48Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424ContiguousLittleEndian, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424PlanarLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_72Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + + [Theory] + [WithFile(FlowerRgb323232Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232ContiguousLittleEndian, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232PlanarLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_96Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs new file mode 100644 index 000000000..71d366369 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public abstract class TiffEncoderBaseTester + { + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + + protected static void TestStripLength( + TestImageProvider provider, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); + TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; + TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + ImageFrame rootFrame = output.Frames.RootFrame; + + Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + Assert.True(output.Height > (int)rowsPerStrip); + Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); + Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; + Assert.NotNull(stripByteCounts); + Assert.True(stripByteCounts.Length > 1); + Assert.NotNull(outputMeta.BitsPerPixel); + + foreach (Number sz in stripByteCounts) + { + Assert.True((uint)sz <= TiffConstants.DefaultStripSize); + } + + // For uncompressed more accurate test. + if (compression == TiffCompression.None) + { + for (int i = 0; i < stripByteCounts.Length - 1; i++) + { + // The difference must be less than one row. + int stripBytes = (int)stripByteCounts[i]; + int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; + + Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); + } + } + + // Compare with reference. + TestTiffEncoderCore( + provider, + inputMeta.BitsPerPixel, + photometricInterpretation, + inputCompression, + useExactComparer: useExactComparer, + compareTolerance: compareTolerance); + } + + protected static void TestTiffEncoderCore( + TestImageProvider provider, + TiffBitsPerPixel? bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression = TiffCompression.None, + TiffPredictor predictor = TiffPredictor.None, + bool useExactComparer = true, + float compareTolerance = 0.001f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + var encoder = new TiffEncoder + { + PhotometricInterpretation = photometricInterpretation, + BitsPerPixel = bitsPerPixel, + Compression = compression, + HorizontalPredictor = predictor + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs new file mode 100644 index 000000000..aeca38c5c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffEncoderMultiframeTests : TiffEncoderBaseTester + { + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] + [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + + [Theory] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_WithPreview(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgb24)] + [WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Convert(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit48, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_RemoveFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Assert.True(image.Frames.Count > 1); + + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_AddFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Assert.Equal(1, image.Frames.Count); + + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image1.Frames.RootFrame); + image.Frames.AddFrame(image2.Frames.RootFrame); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithBlankImages(100, 100, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Create(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + using var image0 = new Image(image.Width, image.Height, Color.Red.ToRgba32()); + + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image0.Frames.RootFrame); + image.Frames.AddFrame(image1.Frames.RootFrame); + image.Frames.AddFrame(image2.Frames.RootFrame); + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit8; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Lzw + }; + + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame0 = output.Frames[0]; + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Red.ToRgba32(), frame0[10, 10]); + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Lzw, frame0.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame0.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 0286671ae..47b6fcf72 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -2,14 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; @@ -19,10 +14,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Collection("RunSerial")] [Trait("Format", "Tiff")] - public class TiffEncoderTests + public class TiffEncoderTests : TiffEncoderBaseTester { - private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); - [Theory] [InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] @@ -451,96 +444,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } - - private static void TestStripLength( - TestImageProvider provider, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression, - bool useExactComparer = true, - float compareTolerance = 0.01f) - where TPixel : unmanaged, IPixel - { - // arrange - var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; - using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); - TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); - TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; - - // act - input.Save(memStream, tiffEncoder); - - // assert - memStream.Position = 0; - using var output = Image.Load(memStream); - ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; - TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); - ImageFrame rootFrame = output.Frames.RootFrame; - - Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; - Assert.True(output.Height > (int)rowsPerStrip); - Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); - Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; - Assert.NotNull(stripByteCounts); - Assert.True(stripByteCounts.Length > 1); - Assert.NotNull(outputMeta.BitsPerPixel); - - foreach (Number sz in stripByteCounts) - { - Assert.True((uint)sz <= TiffConstants.DefaultStripSize); - } - - // For uncompressed more accurate test. - if (compression == TiffCompression.None) - { - for (int i = 0; i < stripByteCounts.Length - 1; i++) - { - // The difference must be less than one row. - int stripBytes = (int)stripByteCounts[i]; - int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; - - Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); - } - } - - // Compare with reference. - TestTiffEncoderCore( - provider, - inputMeta.BitsPerPixel, - photometricInterpretation, - inputCompression, - useExactComparer: useExactComparer, - compareTolerance: compareTolerance); - } - - private static void TestTiffEncoderCore( - TestImageProvider provider, - TiffBitsPerPixel? bitsPerPixel, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression = TiffCompression.None, - TiffPredictor predictor = TiffPredictor.None, - bool useExactComparer = true, - float compareTolerance = 0.001f, - IImageDecoder imageDecoder = null) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - var encoder = new TiffEncoder - { - PhotometricInterpretation = photometricInterpretation, - BitsPerPixel = bitsPerPixel, - Compression = compression, - HorizontalPredictor = predictor - }; - - // Does DebugSave & load reference CompareToReferenceInput(): - image.VerifyEncoder( - provider, - "tiff", - bitsPerPixel, - encoder, - useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), - referenceDecoder: imageDecoder ?? ReferenceDecoder); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index d1a71a9bd..bac59c0c9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -279,8 +279,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); - Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution), encodedImageExifProfile.GetValue(ExifTag.XResolution)); - Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution), encodedImageExifProfile.GetValue(ExifTag.YResolution)); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); Assert.Equal(xmpProfileInput, encodedImageXmpProfile); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 37443a5b4..0465cae94 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -43,6 +43,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering { KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) } }; + public static readonly TheoryData DefaultInstanceDitherers + = new TheoryData + { + default(ErrorDither), + default(OrderedDither) + }; + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f); private static IDither DefaultDitherer => KnownDitherings.Bayer4x4; @@ -175,5 +182,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering c => c.Dither(dither), name); } + + [Theory] + [MemberData(nameof(DefaultInstanceDitherers))] + public void ShouldThrowForDefaultDitherInstance(IDither dither) + { + void Command() + { + using var image = new Image(10, 10); + image.Mutate(x => x.Dither(dither)); + } + + Assert.Throws(Command); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index af1d7f3f3..c99e10138 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -5,6 +5,7 @@ using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -152,6 +153,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization new WuQuantizer(OrderedDitherOptions), }; + public static readonly TheoryData DefaultInstanceDitherers + = new TheoryData + { + default(ErrorDither), + default(OrderedDither) + }; + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); [Theory] @@ -217,5 +225,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization testOutputDetails: testOutputDetails, appendPixelTypeToFileName: false); } + + [Theory] + [MemberData(nameof(DefaultInstanceDitherers))] + public void ShouldThrowForDefaultDitherInstance(IDither dither) + { + void Command() + { + using var image = new Image(10, 10); + var quantizer = new WebSafePaletteQuantizer(); + quantizer.Options.Dither = dither; + image.Mutate(x => x.Quantize(quantizer)); + } + + Assert.Throws(Command); + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 02bb3527c..6d2c1c655 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -158,6 +158,7 @@ namespace SixLabors.ImageSharp.Tests 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 { @@ -199,6 +200,7 @@ namespace SixLabors.ImageSharp.Tests 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 = { @@ -752,8 +754,18 @@ namespace SixLabors.ImageSharp.Tests public const string RgbPalette = "Tiff/rgb_palette.tiff"; public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; + public const string FlowerRgb323232Contiguous = "Tiff/flower-rgb-contig-32.tiff"; + public const string FlowerRgb323232ContiguousLittleEndian = "Tiff/flower-rgb-contig-32_lsb.tiff"; + public const string FlowerRgb323232Planar = "Tiff/flower-rgb-planar-32.tiff"; + public const string FlowerRgb323232PlanarLittleEndian = "Tiff/flower-rgb-planar-32_lsb.tiff"; + public const string FlowerRgb242424Planar = "Tiff/flower-rgb-planar-24.tiff"; + public const string FlowerRgb242424PlanarLittleEndian = "Tiff/flower-rgb-planar-24_lsb.tiff"; + public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; + public const string FlowerRgb242424ContiguousLittleEndian = "Tiff/flower-rgb-contig-24_lsb.tiff"; public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff"; + public const string FlowerRgb161616ContiguousLittleEndian = "Tiff/flower-rgb-contig-16_lsb.tiff"; public const string FlowerRgb161616Planar = "Tiff/flower-rgb-planar-16.tiff"; + public const string FlowerRgb161616PlanarLittleEndian = "Tiff/flower-rgb-planar-16_lsb.tiff"; public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff"; public const string FlowerRgb141414Planar = "Tiff/flower-rgb-planar-14.tiff"; public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff"; @@ -763,6 +775,8 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; public const string FlowerRgb222Planar = "Tiff/flower-rgb-planar-02.tiff"; + public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; + public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string Flower2BitGray = "Tiff/flower-minisblack-02.tiff"; public const string Flower2BitPalette = "Tiff/flower-palette-02.tiff"; public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff"; @@ -773,6 +787,16 @@ namespace SixLabors.ImageSharp.Tests public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff"; public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; + public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; + public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; + public const string Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16.tiff"; + public const string Flower24BitGray = "Tiff/flower-minisblack-24.tiff"; + public const string Flower24BitGrayLittleEndian = "Tiff/flower-minisblack-24_lsb.tiff"; + public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; + public const string Flower32BitGrayLittleEndian = "Tiff/flower-minisblack-32_lsb.tiff"; + public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32.tiff"; + public const string Flower32BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-32_lsb.tiff"; + public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; 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 000000000..a487bb9e7 --- /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 000000000..bc08d8be0 --- /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 diff --git a/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff new file mode 100644 index 000000000..62061bfaf --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3806304a5453a6ec8a6795bc77b967b9aa8593288af36bbf9802f22ee27869e +size 6588 diff --git a/tests/Images/Input/Tiff/flower-minisblack-24.tiff b/tests/Images/Input/Tiff/flower-minisblack-24.tiff new file mode 100644 index 000000000..1fddb22e3 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5a96942ee27a2b25d3cbb8bdd05239be71f84acc4d63c95380841a8a67befd +size 9770 diff --git a/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff new file mode 100644 index 000000000..7f9dd009d --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe2d4e0d99bdfade966e27bd9583bce39bebb90efa8e7f768ce3cec69aa306e2 +size 9770 diff --git a/tests/Images/Input/Tiff/flower-minisblack-32.tiff b/tests/Images/Input/Tiff/flower-minisblack-32.tiff new file mode 100644 index 000000000..b64616ed2 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6677c372a449fe0324b148385cf0ebaaf33ab4563484ae89831dfeacd80d7c93 +size 12885 diff --git a/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff new file mode 100644 index 000000000..cc3be01d2 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e37a4455e6b61e32720af99127b82aacdc907be91b8ed1d8e1a1f06d6a853211 +size 12885 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-16.tiff b/tests/Images/Input/Tiff/flower-miniswhite-16.tiff new file mode 100644 index 000000000..83266873c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-16.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f2c2afd8f1645717087bd2edbc3e8a46b88a54a4996c0e9350fdd652b5c382 +size 6588 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff new file mode 100644 index 000000000..ec9ceb184 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:435c92b453587e1943940111b66afabf70307beb0e1d65e9701fd9bb753eead2 +size 6588 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32.tiff new file mode 100644 index 000000000..f8cf87553 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514417ead3d6c5c6ca33374ef0bb6ecbe5f875a266519d4cbaa4a6b91033d243 +size 12778 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff new file mode 100644 index 000000000..8c99dda7f --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64c948aa03bc4a24cd1d68bb18b5031c119936154a90f1cb1d9aaabd854c5d9b +size 12778 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff new file mode 100644 index 000000000..967d8bbf3 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0951a9c2207eb6864b6a19ec8513a28a874adddb37c3c06b9fd07831372924e3 +size 19150 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff new file mode 100644 index 000000000..9145c21db --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6368a704b0a629239024f6fbfb30723fa317593ef36ddba05d76302530bd974 +size 28568 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff new file mode 100644 index 000000000..40cf1c9b8 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbb2b4ca6d7eeee4737c6963c99ef68fb6971cf6ccee463427a8246574bc6440 +size 28632 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff new file mode 100644 index 000000000..28461d8d8 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7b9da8ec44da84fc89aed1ad221a5eb130a1f233a1ff8a4a15b41898a0e364f +size 38027 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff new file mode 100644 index 000000000..c602b5c4a --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0876580f9c5d8e13656210582137104daba137c99d55eafb5ebbfa418efa6525 +size 38027 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff new file mode 100644 index 000000000..1a8deed21 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a49cf47fdf2ea43e5cb5a473523e50222fb13ff6a66bda2e4bdd5796f66140d8 +size 9770 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff new file mode 100644 index 000000000..1a8deed21 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a49cf47fdf2ea43e5cb5a473523e50222fb13ff6a66bda2e4bdd5796f66140d8 +size 9770 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff new file mode 100644 index 000000000..425ea42ef --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a60552a7ff37f2c16c43e030e7180872af712f5d9c9c7673e2547049af3da9 +size 19168 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff new file mode 100644 index 000000000..b0b41901c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:752452ac51ad1e836fb81267ab708ff81cf81a4c7e00daeed703f67782b563ec +size 28586 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff new file mode 100644 index 000000000..c615089fd --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72f27af4fe177ebe47bef2af64723497d5a5f44808424bedfc2012fe4e3fc34e +size 28586 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff new file mode 100644 index 000000000..a84b4ab37 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a718ae37d6d7a5bb5702cc75350f6feec3e9cdcd7e22aaa4753c7fe9c2db9aae +size 38035 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff new file mode 100644 index 000000000..5caa0886e --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2241683d74e9a52c5077870731e7bd5a7e7558c2a04fd0edf57da3a583044442 +size 38035