From 4eb4e540125f64a1fd7747b4af3598c8897e8dd7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 20:38:43 +0200 Subject: [PATCH 01/31] Add support for decoding tiff's encoded with LeastSignificantBitFirst compression --- .../ModifiedHuffmanTiffCompression.cs | 7 +++-- .../Compression/Decompressors/T4BitReader.cs | 29 ++++++++++++++++--- .../Decompressors/T4TiffCompression.cs | 13 +++++++-- .../Compression/TiffDecompressorsFactory.cs | 7 +++-- .../Formats/Tiff/TiffDecoderCore.cs | 18 ++++++++++-- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 5 ++-- .../Formats/Tiff/TiffDecoderTests.cs | 6 ++++ tests/ImageSharp.Tests/TestImages.cs | 3 ++ ...i3p02_huffman_rle_lowerOrderBitsFirst.tiff | 3 ++ .../f8179f8f5e566349cf3583a1ff3ea95c.tiff | 3 ++ tests/Images/Input/Tiff/g3test.tiff | 3 ++ 11 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 tests/Images/Input/Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff create mode 100644 tests/Images/Input/Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff create mode 100644 tests/Images/Input/Tiff/g3test.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs index 017591e53f..9b12dc90ff 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -22,11 +22,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// Initializes a new instance of the class. /// /// The memory allocator. + /// The logical order of bits within a byte. /// The image width. /// The number of bits per pixel. /// The photometric interpretation. - public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation) - : base(allocator, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation) + public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, fillOrder, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation) { bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; this.whiteValue = (byte)(isWhiteZero ? 0 : 1); @@ -36,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) { - using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true); + using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true); buffer.Clear(); uint bitsWritten = 0; diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs index 09f8c71f72..384be1cf24 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -5,7 +5,8 @@ using System; using System.Buffers; using System.Collections.Generic; using System.IO; - +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors @@ -20,6 +21,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// private int bitsRead; + /// + /// The logical order of bits within a byte. + /// + private readonly TiffFillOrder fillOrder; + /// /// Current value. /// @@ -221,12 +227,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// Initializes a new instance of the class. /// /// The compressed input stream. + /// The logical order of bits within a byte. /// The number of bytes to read from the stream. /// The memory allocator. /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. /// Indicates, if its the modified huffman code variation. Defaults to false. - public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false) + public T4BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false) { + this.fillOrder = fillOrder; this.Data = allocator.Allocate(bytesToRead); this.ReadImageDataFromStream(input, bytesToRead); @@ -375,7 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors break; } - var currBit = this.ReadValue(1); + uint currBit = this.ReadValue(1); this.value = (this.value << 1) | currBit; if (this.IsEndOfScanLine) @@ -816,7 +824,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors Span dataSpan = this.Data.GetSpan(); int shift = 8 - this.bitsRead - 1; - var bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0); + uint bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0); this.bitsRead++; return bit; @@ -837,6 +845,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { Span dataSpan = this.Data.GetSpan(); input.Read(dataSpan, 0, bytesToRead); + + if (this.fillOrder == TiffFillOrder.LeastSignificantBitFirst) + { + for (int i = 0; i < dataSpan.Length; i++) + { + dataSpan[i] = ReverseBits(dataSpan[i]); + } + } } + + // http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64Bits + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ReverseBits(byte b) => + (byte)((((b * 0x80200802UL) & 0x0884422110UL) * 0x0101010101UL) >> 32); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs index 76f0883643..d95fea29bf 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -24,20 +24,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// Initializes a new instance of the class. /// /// The memory allocator. + /// The logical order of bits within a byte. /// The image width. /// The number of bits per pixel. /// Fax compression options. /// The photometric interpretation. - public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation) + public T4TiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation) : base(allocator, width, bitsPerPixel) { this.faxCompressionOptions = faxOptions; + this.FillOrder = fillOrder; bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; this.whiteValue = (byte)(isWhiteZero ? 0 : 1); this.blackValue = (byte)(isWhiteZero ? 1 : 0); } + /// + /// Gets the logical order of bits within a byte. + /// + protected TiffFillOrder FillOrder { get; } + /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) { @@ -46,8 +53,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported"); } - var eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding); - using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding); + bool eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding); + using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding); buffer.Clear(); uint bitsWritten = 0; diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index a6d44f4d3c..ff04edab70 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -16,7 +16,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression int width, int bitsPerPixel, TiffPredictor predictor, - FaxCompressionOptions faxOptions) + FaxCompressionOptions faxOptions, + TiffFillOrder fillOrder) { switch (method) { @@ -40,11 +41,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffDecoderCompressionType.T4: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); - return new T4TiffCompression(allocator, width, bitsPerPixel, faxOptions, photometricInterpretation); + return new T4TiffCompression(allocator, fillOrder, width, bitsPerPixel, faxOptions, photometricInterpretation); case TiffDecoderCompressionType.HuffmanRle: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); - return new ModifiedHuffmanTiffCompression(allocator, width, bitsPerPixel, photometricInterpretation); + return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); default: throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 011d037796..9fb8c6cebd 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -85,6 +85,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public FaxCompressionOptions FaxCompressionOptions { get; set; } + /// + /// Gets or sets the the logical order of bits within a byte. + /// + public TiffFillOrder FillOrder { get; set; } + /// /// Gets or sets the planar configuration type to use when decoding the image. /// @@ -264,7 +269,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frame.Width, + bitsPerPixel, + this.Predictor, + this.FaxCompressionOptions, + this.FillOrder); TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); @@ -314,7 +327,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff frame.Width, bitsPerPixel, this.Predictor, - this.FaxCompressionOptions); + this.FaxCompressionOptions, + this.FillOrder); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 14c527a34c..5496b32bfd 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -35,9 +35,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff } TiffFillOrder fillOrder = (TiffFillOrder?)exifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst; - if (fillOrder != TiffFillOrder.MostSignificantBitFirst) + if (fillOrder == TiffFillOrder.LeastSignificantBitFirst && frameMetadata.BitsPerPixel != TiffBitsPerPixel.Bit1) { - TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported."); + TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is only supported in combination with 1bit per pixel bicolor tiff's."); } if (frameMetadata.Predictor == TiffPredictor.FloatingPoint) @@ -69,6 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); + options.FillOrder = fillOrder; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 67892b14b1..cae1597a50 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -221,6 +221,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_Fax3Compressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(CcittFax3LowerOrderBitsFirst01, PixelTypes.Rgba32)] + [WithFile(CcittFax3LowerOrderBitsFirst02, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Compressed_LowerOrderBitsFirst(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Calliphora_RgbPackbits, PixelTypes.Rgba32)] [WithFile(RgbPackbits, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 929e375246..d24c32b55e 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -539,6 +539,9 @@ namespace SixLabors.ImageSharp.Tests public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff"; public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff"; public const string HuffmanRleAllMakeupCodes = "Tiff/huffman_rle_all_makeup_codes.tiff"; + public const string CcittFax3LowerOrderBitsFirst01 = "Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff"; + public const string CcittFax3LowerOrderBitsFirst02 = "Tiff/g3test.tiff"; + public const string HuffmanRleLowerOrderBitsFirst = "Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff"; // Test case for an issue, that the last bits in a row got ignored. public const string HuffmanRle_basi3p02 = "Tiff/basi3p02_huffman_rle.tiff"; diff --git a/tests/Images/Input/Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff b/tests/Images/Input/Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff new file mode 100644 index 0000000000..6ab0603246 --- /dev/null +++ b/tests/Images/Input/Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ac3e56a93996464a579ae19cf5f8d9531e2f08db36879aaba176731c24951a5 +size 352 diff --git a/tests/Images/Input/Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff b/tests/Images/Input/Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff new file mode 100644 index 0000000000..9dc10018e4 --- /dev/null +++ b/tests/Images/Input/Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf75c4b679d2449e239f228cdee6a25adc7d7b16dde3fb9061a07b2fb0699db1 +size 735412 diff --git a/tests/Images/Input/Tiff/g3test.tiff b/tests/Images/Input/Tiff/g3test.tiff new file mode 100644 index 0000000000..62207de3ad --- /dev/null +++ b/tests/Images/Input/Tiff/g3test.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b2e1a17338133aa95cb8a16d82a171f5b50f7b9ae1a51ab06227dc3daa81d5 +size 50401 From 4439c3804229f534e0d416cede087484719c18f7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 21:26:39 +0200 Subject: [PATCH 02/31] Update readme --- src/ImageSharp/Formats/Tiff/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 5b116b819a..7b7c0efd53 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -25,7 +25,7 @@ ## Implementation Status -- The Decoder and Encoder currently only supports a single frame per image. +- The Decoder currently only supports a single frame per image. - Some compression formats are not yet supported. See the list below. ### Deviations from the TIFF spec (to be fixed) @@ -81,7 +81,7 @@ |Thresholding | | | | |CellWidth | | | | |CellLength | | | | -|FillOrder | | - | Ignore. In practice is very uncommon, and is not recommended. | +|FillOrder | | Y | | |ImageDescription | Y | Y | | |Make | Y | Y | | |Model | Y | Y | | From af439841b8b37c1f7df772ad8199c4a646194b27 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 10:33:34 +0200 Subject: [PATCH 03/31] Run ArrayPoolMemoryAllocatorTests serial --- .../Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs index 50ec09ce3f..7620d63deb 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs @@ -11,6 +11,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Memory.Allocators { + [Collection("RunSerial")] public class ArrayPoolMemoryAllocatorTests { private const int MaxPooledBufferSizeInBytes = 2048; @@ -56,19 +57,14 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators [Fact] public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown() - { - Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200)); - } + => Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200)); } [Theory] [InlineData(32)] [InlineData(512)] [InlineData(MaxPooledBufferSizeInBytes - 1)] - public void SmallBuffersArePooled_OfByte(int size) - { - Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(size)); - } + public void SmallBuffersArePooled_OfByte(int size) => Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(size)); [Theory] [InlineData(128 * 1024 * 1024)] From 928db20b1fee312103117a71383aee029b712280 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 18:46:04 +0200 Subject: [PATCH 04/31] Use fax3 test image with multiple strips --- tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff index 39852d5345..d2761d2919 100644 --- a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b9b105857723bca5f478a9ab23c0aeca93abe863781019bbd2da47f18c46f24 -size 125778 +oid sha256:bba35f1e43c8425f3bcfab682efae4d2c00c62f0d8a4b411e646d32047469526 +size 125802 From eb63882d4e77574cdbc63962d7b2fe9b84ee1ebf Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 20:19:05 +0200 Subject: [PATCH 05/31] Handle edge case when we are at the last byte position, but not all pixels have been written --- .../Decompressors/T4TiffCompression.cs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs index d95fea29bf..a79ef3fe5e 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -20,6 +20,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private readonly byte blackValue; + private readonly int width; + /// /// Initializes a new instance of the class. /// @@ -34,7 +36,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { this.faxCompressionOptions = faxOptions; this.FillOrder = fillOrder; - + this.width = width; bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; this.whiteValue = (byte)(isWhiteZero ? 0 : 1); this.blackValue = (byte)(isWhiteZero ? 1 : 0); @@ -58,22 +60,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors buffer.Clear(); uint bitsWritten = 0; + uint pixelWritten = 0; while (bitReader.HasMoreData) { bitReader.ReadNextRun(); if (bitReader.RunLength > 0) { - if (bitReader.IsWhiteRun) - { - BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); - bitsWritten += bitReader.RunLength; - } - else - { - BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue); - bitsWritten += bitReader.RunLength; - } + this.WritePixelRun(buffer, bitReader, bitsWritten); + + bitsWritten += bitReader.RunLength; + pixelWritten += bitReader.RunLength; } if (bitReader.IsEndOfScanLine) @@ -85,8 +82,29 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); bitsWritten += pad; } + + pixelWritten = 0; } } + + // Edge case for when we are at the last byte, but there are still some unwritten pixels left. + if (pixelWritten > 0 && pixelWritten < this.width) + { + bitReader.ReadNextRun(); + this.WritePixelRun(buffer, bitReader, bitsWritten); + } + } + + private void WritePixelRun(Span buffer, T4BitReader bitReader, uint bitsWritten) + { + if (bitReader.IsWhiteRun) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); + } + else + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue); + } } /// From ebfb1b5148b4a60175107b2535e1d813a4972532 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 20:19:36 +0200 Subject: [PATCH 06/31] Use smaller test images --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 3 +-- tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs | 6 +++++- tests/ImageSharp.Tests/TestImages.cs | 6 ++---- tests/Images/Input/Tiff/b0350_fillorder2.tiff | 3 --- .../Input/Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff | 3 +++ 5 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 tests/Images/Input/Tiff/b0350_fillorder2.tiff create mode 100644 tests/Images/Input/Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index cae1597a50..6d0f65168e 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -222,8 +222,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] - [WithFile(CcittFax3LowerOrderBitsFirst01, PixelTypes.Rgba32)] - [WithFile(CcittFax3LowerOrderBitsFirst02, PixelTypes.Rgba32)] + [WithFile(CcittFax3LowerOrderBitsFirst, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_Compressed_LowerOrderBitsFirst(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 47b6fcf727..712c8502ab 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -117,7 +117,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] - public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffBitsPerPixel expectedBitsPerPixel, TiffCompression expectedCompression) + public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works( + TiffPhotometricInterpretation? photometricInterpretation, + TiffCompression compression, + TiffBitsPerPixel expectedBitsPerPixel, + TiffCompression expectedCompression) { // arrange var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression }; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index d24c32b55e..4233c69e05 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -539,8 +539,7 @@ namespace SixLabors.ImageSharp.Tests public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff"; public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff"; public const string HuffmanRleAllMakeupCodes = "Tiff/huffman_rle_all_makeup_codes.tiff"; - public const string CcittFax3LowerOrderBitsFirst01 = "Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff"; - public const string CcittFax3LowerOrderBitsFirst02 = "Tiff/g3test.tiff"; + public const string CcittFax3LowerOrderBitsFirst = "Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff"; public const string HuffmanRleLowerOrderBitsFirst = "Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff"; // Test case for an issue, that the last bits in a row got ignored. @@ -607,7 +606,6 @@ namespace SixLabors.ImageSharp.Tests public const string MultiframeDifferentSize = "Tiff/multipage_differentSize.tiff"; public const string MultiframeDifferentVariants = "Tiff/multipage_differentVariants.tiff"; - public const string FillOrder2 = "Tiff/b0350_fillorder2.tiff"; public const string LittleEndianByteOrder = "Tiff/little_endian.tiff"; public const string Fax4_Motorola = "Tiff/moy.tiff"; @@ -622,7 +620,7 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] Metadata = { SampleMetadata }; - public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2, Calliphora_Fax4Compressed, Fax4_Motorola }; + public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, Calliphora_Fax4Compressed, Fax4_Motorola }; } } } diff --git a/tests/Images/Input/Tiff/b0350_fillorder2.tiff b/tests/Images/Input/Tiff/b0350_fillorder2.tiff deleted file mode 100644 index 3b7ee6ac3a..0000000000 --- a/tests/Images/Input/Tiff/b0350_fillorder2.tiff +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37c6a28f460d8781fdc3bcf0cc9bd23f633b03899563546bfc6234a8478f67f0 -size 68637 diff --git a/tests/Images/Input/Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff b/tests/Images/Input/Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff new file mode 100644 index 0000000000..d9b5a5bfb7 --- /dev/null +++ b/tests/Images/Input/Tiff/basi3p02_fax3_lowerOrderBitsFirst.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb56b3582c5c7d91d712e68181110ab0bf74d21992030629f05803c420b7b483 +size 388 From 0559f464dd60b19515dd764fff1f950da36b41da Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 22:31:13 +0200 Subject: [PATCH 07/31] Use smaller test image --- tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff index 5574bd58ed..6f929f3e1d 100644 --- a/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff +++ b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:622d69dba0a8a67aa3b87e384a2b9ea8d29689eaa5cb5d0eee857f98ed660517 -size 15154924 +oid sha256:e4a6c925dd6d293c5d97aac01cdb0ab3f2fb4bbfa4bb1cbe6463545295f5c5fb +size 207979 From 1d8b9101f8ac5403e548335576a3d9e5519b715d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 Aug 2021 11:55:47 +0200 Subject: [PATCH 08/31] Restore broken IPTC data for testimage (which got lost during resize) --- tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff index 6f929f3e1d..24a4141f56 100644 --- a/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff +++ b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4a6c925dd6d293c5d97aac01cdb0ab3f2fb4bbfa4bb1cbe6463545295f5c5fb -size 207979 +oid sha256:579db6b2bd34566846de992f255c6b341d0f88d957a0eb02b01caad3f20c5b44 +size 78794 From 3d02cfdbf05feb6a16f2d45cad127587dca4fa47 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 Aug 2021 20:09:29 +0200 Subject: [PATCH 09/31] Add support for decoding tiff's with 32bit float rgb pixel data --- .../RgbFloat323232TiffColor{TPixel}.cs | 89 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 10 +++ .../TiffColorType.cs | 5 ++ .../Formats/Tiff/TiffDecoderCore.cs | 5 ++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 19 ++-- .../Formats/Tiff/TiffDecoderTests.cs | 11 +++ tests/ImageSharp.Tests/TestImages.cs | 2 + .../Input/Tiff/flower-rgb-float32_lsb.tiff | 3 + .../Input/Tiff/flower-rgb-float32_msb.tiff | 3 + 9 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-float32_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-float32_msb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs new file mode 100644 index 0000000000..739d1059c9 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -0,0 +1,89 @@ +// 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 'RGB' photometric interpretation with 32 bits for each channel. + /// + internal class RgbFloat323232TiffColor : 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 RgbFloat323232TiffColor(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]; + + 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, 4).CopyTo(buffer); + Array.Reverse(buffer); + float r = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float g = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float b = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(r, g, b, 1.0f); + Array.Reverse(buffer); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + float r = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + float g = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + float b = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(r, g, b, 1.0f); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 4a2fe93fe4..e1c813027d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -176,6 +176,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.RgbFloat323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbFloat323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index dc47dc8cd4..07959056c5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -133,6 +133,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// Rgb323232, + /// + /// RGB color image with 32 bits floats for each channel. + /// + RgbFloat323232, + /// /// RGB Full Color. Planar configuration of data. 8 Bit per color channel. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 63185619d4..cb5b82bfd9 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -95,6 +95,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + /// + /// Gets or sets the sample format. + /// + public TiffSampleFormat SampleFormat { get; set; } + /// /// Gets or sets the horizontal predictor. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 8590203a70..8449eb7b2e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -45,14 +45,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("TIFF images with FloatingPoint horizontal predictor are not supported."); } - TiffSampleFormat[] sampleFormat = exifProfile.GetValue(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray(); - if (sampleFormat != null) + TiffSampleFormat[] sampleFormats = exifProfile.GetValue(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray(); + TiffSampleFormat? sampleFormat = null; + if (sampleFormats != null) { - foreach (TiffSampleFormat format in sampleFormat) + sampleFormat = sampleFormats[0]; + foreach (TiffSampleFormat format in sampleFormats) { - if (format != TiffSampleFormat.UnsignedInteger) + if (format != TiffSampleFormat.UnsignedInteger && format != TiffSampleFormat.Float) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger SampleFormat."); + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger and Float SampleFormat."); } } } @@ -67,6 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.PlanarConfiguration = (TiffPlanarConfiguration?)exifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration; options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None; options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; + options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); @@ -231,6 +234,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); } + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.RgbFloat323232; + return; + } + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { ushort bitsPerChannel = options.BitsPerSample.Channel0; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 182fafa33e..9839350382 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -237,6 +237,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(FlowerRgbFloat323232, PixelTypes.Rgba32)] + [WithFile(FlowerRgbFloat323232LittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Float_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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8eaaec4935..74fde6a56a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -563,6 +563,8 @@ 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 FlowerRgbFloat323232 = "Tiff/flower-rgb-float32_msb.tiff"; + public const string FlowerRgbFloat323232LittleEndian = "Tiff/flower-rgb-float32_lsb.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"; diff --git a/tests/Images/Input/Tiff/flower-rgb-float32_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-float32_lsb.tiff new file mode 100644 index 0000000000..da0f104387 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-float32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1f889baa6f3bb99f15609848cdd47d548d3e2ed1b7b558d428550dfa3bd4bf9 +size 38050 diff --git a/tests/Images/Input/Tiff/flower-rgb-float32_msb.tiff b/tests/Images/Input/Tiff/flower-rgb-float32_msb.tiff new file mode 100644 index 0000000000..353771db94 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-float32_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c02be5dff2bcd5d60afbf379ba9095b0c8fd3a7a0063f684ac9ac9119f967a5 +size 38050 From 9d6b7a6204a146c32137df9db7076e2d43eef127 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 Aug 2021 20:34:01 +0200 Subject: [PATCH 10/31] Add support for decoding tiff's with 32bit float gray pixel data with min is black --- .../BlackIsZero32FloatTiffColor{TPixel}.cs | 69 +++++++++++++++++++ .../BlackIsZero32TiffColor{TPixel}.cs | 1 - .../RgbFloat323232TiffColor{TPixel}.cs | 1 - .../TiffColorDecoderFactory{TPixel}.cs | 5 ++ .../TiffColorType.cs | 5 ++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 18 +++-- .../Formats/Tiff/TiffDecoderTests.cs | 11 +++ tests/ImageSharp.Tests/TestImages.cs | 2 + .../Tiff/flower-minisblack-float32_lsb.tiff | 3 + .../Tiff/flower-minisblack-float32_msb.tiff | 3 + 10 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-minisblack-float32_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-minisblack-float32_msb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs new file mode 100644 index 0000000000..ff34a29eb2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -0,0 +1,69 @@ +// 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 float grayscale images. + /// + internal class BlackIsZero32FloatTiffColor : 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 BlackIsZero32FloatTiffColor(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 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, 4).CopyTo(buffer); + Array.Reverse(buffer); + float intensity = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + float intensity = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs index 862756bc42..f54a794840 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // 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; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs index 739d1059c9..f3f27d5c4b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -57,7 +57,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(r, g, b, 1.0f); - Array.Reverse(buffer); color.FromVector4(colorVector); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index e1c813027d..f3cc4c01c8 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -82,6 +82,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new BlackIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero32Float: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 07959056c5..0d742c2b95 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -43,6 +43,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// BlackIsZero32, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Pixel data is 32-bit float. + /// + BlackIsZero32Float, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 8449eb7b2e..79c4d372d5 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -177,6 +177,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff { case 32: { + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.BlackIsZero32Float; + return; + } + options.ColorType = TiffColorType.BlackIsZero32; break; } @@ -234,18 +240,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); } - if (options.SampleFormat == TiffSampleFormat.Float) - { - options.ColorType = TiffColorType.RgbFloat323232; - return; - } - if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { ushort bitsPerChannel = options.BitsPerSample.Channel0; switch (bitsPerChannel) { case 32: + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.RgbFloat323232; + return; + } + options.ColorType = TiffColorType.Rgb323232; break; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 9839350382..e45b994a62 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -248,6 +248,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(Flower32BitFloatGray, PixelTypes.Rgba32)] + [WithFile(Flower32BitFloatGrayLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Float_96Bit_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(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 74fde6a56a..eb83062d95 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -605,6 +605,8 @@ namespace SixLabors.ImageSharp.Tests 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 Flower32BitFloatGray = "Tiff/flower-minisblack-float32_msb.tiff"; + public const string Flower32BitFloatGrayLittleEndian = "Tiff/flower-minisblack-float32_lsb.tiff"; public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32.tiff"; public const string Flower32BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-32_lsb.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-float32_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-float32_lsb.tiff new file mode 100644 index 0000000000..8e94faea0b --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-float32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af82e07e6298082c91d60a97acb29b67ecabf386bc14371fcb698b248cfd3b40 +size 12814 diff --git a/tests/Images/Input/Tiff/flower-minisblack-float32_msb.tiff b/tests/Images/Input/Tiff/flower-minisblack-float32_msb.tiff new file mode 100644 index 0000000000..464074c153 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-float32_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c918b8aa9968c03c12d85b3bcacd35ae57663a19f5490fc1c351521ed835f30 +size 12814 From b29581e57a759d7fa2dcff7a483a28f8d3a78f3d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 Aug 2021 20:54:32 +0200 Subject: [PATCH 11/31] Add support for decoding tiff's with 32bit float gray pixel data with min is white --- .../TiffColorDecoderFactory{TPixel}.cs | 5 ++ .../TiffColorType.cs | 5 ++ .../WhiteIsZero32FloatTiffColor{TPixel}.cs | 69 +++++++++++++++++++ .../WhiteIsZero8TiffColor{TPixel}.cs | 2 +- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 6 ++ .../Formats/Tiff/TiffDecoderTests.cs | 2 + tests/ImageSharp.Tests/TestImages.cs | 2 + .../Tiff/flower-miniswhite-float32_lsb.tiff | 3 + .../Tiff/flower-miniswhite-float32_msb.tiff | 3 + 9 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-float32_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-float32_msb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index f3cc4c01c8..e27c0a61cf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -47,6 +47,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new WhiteIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.WhiteIsZero32Float: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero: DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 0d742c2b95..81db744a1e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -83,6 +83,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// WhiteIsZero32, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Pixel data is 32-bit float. + /// + WhiteIsZero32Float, + /// /// Palette-color. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs new file mode 100644 index 0000000000..d532247fe3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -0,0 +1,69 @@ +// 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 'WhiteIsZero' photometric interpretation for 32-bit float grayscale images. + /// + internal class WhiteIsZero32FloatTiffColor : 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 WhiteIsZero32FloatTiffColor(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 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, 4).CopyTo(buffer); + Array.Reverse(buffer); + float intensity = 1.0f - BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + float intensity = 1.0f - BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); + color.FromVector4(colorVector); + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs index 6a6c2af225..15ebed58f9 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - byte intensity = (byte)(255 - data[offset++]); + byte intensity = (byte)(byte.MaxValue - data[offset++]); pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 79c4d372d5..5fa28f4186 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -116,6 +116,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff { case 32: { + if (options.SampleFormat == TiffSampleFormat.Float) + { + options.ColorType = TiffColorType.WhiteIsZero32Float; + return; + } + options.ColorType = TiffColorType.WhiteIsZero32; break; } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index e45b994a62..350b089164 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -251,6 +251,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower32BitFloatGray, PixelTypes.Rgba32)] [WithFile(Flower32BitFloatGrayLittleEndian, PixelTypes.Rgba32)] + [WithFile(Flower32BitFloatGrayMinIsWhite, PixelTypes.Rgba32)] + [WithFile(Flower32BitFloatGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_Float_96Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index eb83062d95..f1c852da34 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -607,6 +607,8 @@ namespace SixLabors.ImageSharp.Tests public const string Flower32BitGrayLittleEndian = "Tiff/flower-minisblack-32_lsb.tiff"; public const string Flower32BitFloatGray = "Tiff/flower-minisblack-float32_msb.tiff"; public const string Flower32BitFloatGrayLittleEndian = "Tiff/flower-minisblack-float32_lsb.tiff"; + public const string Flower32BitFloatGrayMinIsWhite = "Tiff/flower-miniswhite-float32_msb.tiff"; + public const string Flower32BitFloatGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-float32_lsb.tiff"; public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32.tiff"; public const string Flower32BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-32_lsb.tiff"; diff --git a/tests/Images/Input/Tiff/flower-miniswhite-float32_lsb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-float32_lsb.tiff new file mode 100644 index 0000000000..3241c8fbec --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-float32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e06533486f15e33f2435d081713fbecc3ba96c842058b7ba3a5d9116fe5f5c +size 12814 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-float32_msb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-float32_msb.tiff new file mode 100644 index 0000000000..5623a74286 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-float32_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800a101f4d23fa2a499fcef036ebfca7d9338ac71b06a32ad05e7eb1905ddae3 +size 12814 From b842c07fa866bd435a11dc0381e2ed7960fc4951 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 Aug 2021 16:18:45 +0200 Subject: [PATCH 12/31] Fix issue with white is zero with 24 and 32 bit --- .../WhiteIsZero24TiffColor{TPixel}.cs | 5 +++-- .../WhiteIsZero32TiffColor{TPixel}.cs | 5 +++-- tests/ImageSharp.Tests/TestImages.cs | 8 ++++---- ...r-minisblack-24.tiff => flower-minisblack-24_msb.tiff} | 0 ...r-minisblack-32.tiff => flower-minisblack-32_msb.tiff} | 0 ...r-miniswhite-16.tiff => flower-miniswhite-16_msb.tiff} | 0 tests/Images/Input/Tiff/flower-miniswhite-32.tiff | 3 --- tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff | 2 +- tests/Images/Input/Tiff/flower-miniswhite-32_msb.tiff | 3 +++ 9 files changed, 14 insertions(+), 12 deletions(-) rename tests/Images/Input/Tiff/{flower-minisblack-24.tiff => flower-minisblack-24_msb.tiff} (100%) rename tests/Images/Input/Tiff/{flower-minisblack-32.tiff => flower-minisblack-32_msb.tiff} (100%) rename tests/Images/Input/Tiff/{flower-miniswhite-16.tiff => flower-miniswhite-16_msb.tiff} (100%) delete mode 100644 tests/Images/Input/Tiff/flower-miniswhite-32.tiff create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-32_msb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs index b1088732ba..10182f250f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -31,6 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation color.FromVector4(TiffUtils.Vector4Default); byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; + const uint maxValue = 0xFFFFFF; Span bufferSpan = buffer.AsSpan(bufferStartIdx); int offset = 0; @@ -42,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 3).CopyTo(bufferSpan); - ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); + ulong intensity = maxValue - TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); @@ -53,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 3).CopyTo(bufferSpan); - ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); + ulong intensity = maxValue - 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 index 0071740036..ef62b4f441 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -29,6 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); color.FromVector4(TiffUtils.Vector4Default); + const uint maxValue = 0xFFFFFFFF; int offset = 0; for (int y = top; y < top + height; y++) @@ -38,7 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong intensity = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + ulong intensity = maxValue - TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); offset += 4; pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); @@ -48,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong intensity = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + ulong intensity = maxValue - TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); offset += 4; pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index f1c852da34..a25306cc9c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -600,16 +600,16 @@ namespace SixLabors.ImageSharp.Tests 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 Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16_msb.tiff"; + public const string Flower24BitGray = "Tiff/flower-minisblack-24_msb.tiff"; public const string Flower24BitGrayLittleEndian = "Tiff/flower-minisblack-24_lsb.tiff"; - public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; + public const string Flower32BitGray = "Tiff/flower-minisblack-32_msb.tiff"; public const string Flower32BitGrayLittleEndian = "Tiff/flower-minisblack-32_lsb.tiff"; public const string Flower32BitFloatGray = "Tiff/flower-minisblack-float32_msb.tiff"; public const string Flower32BitFloatGrayLittleEndian = "Tiff/flower-minisblack-float32_lsb.tiff"; public const string Flower32BitFloatGrayMinIsWhite = "Tiff/flower-miniswhite-float32_msb.tiff"; public const string Flower32BitFloatGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-float32_lsb.tiff"; - public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32.tiff"; + public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32_msb.tiff"; public const string Flower32BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-32_lsb.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-24.tiff b/tests/Images/Input/Tiff/flower-minisblack-24_msb.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-minisblack-24.tiff rename to tests/Images/Input/Tiff/flower-minisblack-24_msb.tiff diff --git a/tests/Images/Input/Tiff/flower-minisblack-32.tiff b/tests/Images/Input/Tiff/flower-minisblack-32_msb.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-minisblack-32.tiff rename to tests/Images/Input/Tiff/flower-minisblack-32_msb.tiff diff --git a/tests/Images/Input/Tiff/flower-miniswhite-16.tiff b/tests/Images/Input/Tiff/flower-miniswhite-16_msb.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-miniswhite-16.tiff rename to tests/Images/Input/Tiff/flower-miniswhite-16_msb.tiff diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32.tiff deleted file mode 100644 index f8cf87553e..0000000000 --- a/tests/Images/Input/Tiff/flower-miniswhite-32.tiff +++ /dev/null @@ -1,3 +0,0 @@ -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 index 8c99dda7fb..d44ae9b915 100644 --- a/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff +++ b/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64c948aa03bc4a24cd1d68bb18b5031c119936154a90f1cb1d9aaabd854c5d9b +oid sha256:17139bc00d9fe2905fbac9226120d2823ea17a10a39b140970122153ec23265d size 12778 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32_msb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32_msb.tiff new file mode 100644 index 0000000000..d44ae9b915 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-32_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17139bc00d9fe2905fbac9226120d2823ea17a10a39b140970122153ec23265d +size 12778 From 1aea0061ebd9e3fb34359b18321678d0b236999c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 Aug 2021 19:23:32 +0200 Subject: [PATCH 13/31] Add support for horizontal predictor for 16 bit gray tiff's --- .../Decompressors/DeflateTiffCompression.cs | 12 ++--- .../Decompressors/LzwTiffCompression.cs | 11 ++-- .../Tiff/Compression/HorizontalPredictor.cs | 54 ++++++++++++++++++- .../Compression/TiffDecompressorsFactory.cs | 7 +-- .../Formats/Tiff/TiffDecoderCore.cs | 13 ++++- .../DeflateTiffCompressionTests.cs | 2 +- .../Compression/LzwTiffCompressionTests.cs | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 6 +++ tests/ImageSharp.Tests/TestImages.cs | 2 + ...lower-minisblack-16_lsb_lzw_predictor.tiff | 3 ++ ...lower-minisblack-16_msb_lzw_predictor.tiff | 3 ++ 11 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 tests/Images/Input/Tiff/flower-minisblack-16_lsb_lzw_predictor.tiff create mode 100644 tests/Images/Input/Tiff/flower-minisblack-16_msb_lzw_predictor.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index 2188913bc3..ef99832f70 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -5,7 +5,6 @@ using System; using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -20,6 +19,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// internal class DeflateTiffCompression : TiffBaseDecompressor { + private readonly bool isBigEndian; + /// /// Initializes a new instance of the class. /// @@ -27,10 +28,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// The image width. /// The bits used per pixel. /// The tiff predictor used. - public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor) - : base(memoryAllocator, width, bitsPerPixel, predictor) - { - } + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor, bool isBigEndian) + : base(memoryAllocator, width, bitsPerPixel, predictor) => this.isBigEndian = isBigEndian; /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) @@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel); + HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel, this.isBigEndian); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs index 7e75dd4f05..0fe1b60ecf 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// internal class LzwTiffCompression : TiffBaseDecompressor { + private readonly bool isBigEndian; + /// /// Initializes a new instance of the class. /// @@ -20,10 +22,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// The image width. /// The bits used per pixel. /// The tiff predictor used. - public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor) - : base(memoryAllocator, width, bitsPerPixel, predictor) - { - } + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor, bool isBigEndian) + : base(memoryAllocator, width, bitsPerPixel, predictor) => this.isBigEndian = isBigEndian; /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) @@ -33,7 +34,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel); + HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel, this.isBigEndian); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index ae2f17dbb5..da84b51b39 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -2,9 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Compression @@ -20,12 +21,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// Buffer with decompressed pixel data. /// The width of the image or strip. /// Bits per pixel. - public static void Undo(Span pixelBytes, int width, int bitsPerPixel) + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public static void Undo(Span pixelBytes, int width, int bitsPerPixel, bool isBigEndian) { if (bitsPerPixel == 8) { Undo8Bit(pixelBytes, width); } + else if (bitsPerPixel == 16) + { + Undo16Bit(pixelBytes, width, isBigEndian); + } else if (bitsPerPixel == 24) { Undo24Bit(pixelBytes, width); @@ -110,6 +116,50 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } + private static void Undo16Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtils.ConvertToUShortBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, pixelValue); + offset += 2; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, pixelValue); + offset += 2; + } + } + } + } + private static void Undo24Bit(Span pixelBytes, int width) { int rowBytesCount = width * 3; diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index a6d44f4d3c..964f0bf127 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -16,7 +16,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression int width, int bitsPerPixel, TiffPredictor predictor, - FaxCompressionOptions faxOptions) + FaxCompressionOptions faxOptions, + ByteOrder byteOrder) { switch (method) { @@ -32,11 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffDecoderCompressionType.Deflate: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new DeflateTiffCompression(allocator, width, bitsPerPixel, predictor); + return new DeflateTiffCompression(allocator, width, bitsPerPixel, predictor, byteOrder == ByteOrder.BigEndian); case TiffDecoderCompressionType.Lzw: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new LzwTiffCompression(allocator, width, bitsPerPixel, predictor); + return new LzwTiffCompression(allocator, width, bitsPerPixel, predictor, byteOrder == ByteOrder.BigEndian); case TiffDecoderCompressionType.T4: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 63185619d4..2c7e8d0c8f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -264,7 +264,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frame.Width, + bitsPerPixel, + this.Predictor, + this.FaxCompressionOptions, + this.byteOrder); TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); @@ -314,7 +322,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff frame.Width, bitsPerPixel, this.Predictor, - this.FaxCompressionOptions); + this.FaxCompressionOptions, + this.byteOrder); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.Configuration, this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs index 782a504d58..e9399e23b5 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression { var buffer = new byte[data.Length]; - using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None); + using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None, false); decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index bf585e9c8f..834d206cc7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression using BufferedReadStream stream = CreateCompressedStream(data); var buffer = new byte[data.Length]; - using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None); + using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None, false); decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); Assert.Equal(data, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 182fafa33e..bba27039dd 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -161,6 +161,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_16Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower16BitGrayPredictorBigEndian, PixelTypes.Rgba32)] + [WithFile(Flower16BitGrayPredictorLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_16Bit_Gray_WithPredictor(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower24BitGray, PixelTypes.Rgba32)] [WithFile(Flower24BitGrayLittleEndian, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8eaaec4935..08b0b2901f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -599,6 +599,8 @@ namespace SixLabors.ImageSharp.Tests 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 Flower16BitGrayPredictorBigEndian = "Tiff/flower-minisblack-16_msb_lzw_predictor.tiff"; + public const string Flower16BitGrayPredictorLittleEndian = "Tiff/flower-minisblack-16_lsb_lzw_predictor.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"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-16_lsb_lzw_predictor.tiff b/tests/Images/Input/Tiff/flower-minisblack-16_lsb_lzw_predictor.tiff new file mode 100644 index 0000000000..e28ccda483 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-16_lsb_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263da6b84862af1bc23f2631e648b1a629b5571b253c82f3ea64f44e9b7b1fe6 +size 8478 diff --git a/tests/Images/Input/Tiff/flower-minisblack-16_msb_lzw_predictor.tiff b/tests/Images/Input/Tiff/flower-minisblack-16_msb_lzw_predictor.tiff new file mode 100644 index 0000000000..144f96887b --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-16_msb_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e67ac19cbdeb8585b204ee958edc7679a92c2b415a1a2c6051f14fe2966f933c +size 8504 From 78efd684d78443fb612d6cc1cfd6d9a9a8cd9ab4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 Aug 2021 20:40:32 +0200 Subject: [PATCH 14/31] Add support for horizontal predictor for 32 bit gray tiff's --- .../Decompressors/DeflateTiffCompression.cs | 14 +++- .../Decompressors/LzwTiffCompression.cs | 14 +++- .../Tiff/Compression/HorizontalPredictor.cs | 82 +++++++++++++++---- .../Compression/TiffDecompressorsFactory.cs | 6 +- .../BlackIsZero32TiffColor{TPixel}.cs | 1 - .../Formats/Tiff/TiffDecoderCore.cs | 2 + .../DeflateTiffCompressionTests.cs | 3 +- .../Compression/LzwTiffCompressionTests.cs | 3 +- .../Formats/Tiff/TiffDecoderTests.cs | 11 +++ tests/ImageSharp.Tests/TestImages.cs | 2 + ...r-minisblack-32_lsb_deflate_predictor.tiff | 3 + ...r-minisblack-32_msb_deflate_predictor.tiff | 3 + 12 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 tests/Images/Input/Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff create mode 100644 tests/Images/Input/Tiff/flower-minisblack-32_msb_deflate_predictor.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index ef99832f70..bb57853d53 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -6,6 +6,7 @@ using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -21,16 +22,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { private readonly bool isBigEndian; + private readonly TiffColorType colorType; + /// /// Initializes a new instance of the class. /// /// The memoryAllocator to use for buffer allocations. /// The image width. /// The bits used per pixel. + /// The color type of the pixel data. /// The tiff predictor used. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor, bool isBigEndian) - : base(memoryAllocator, width, bitsPerPixel, predictor) => this.isBigEndian = isBigEndian; + public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + this.colorType = colorType; + this.isBigEndian = isBigEndian; + } /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) @@ -62,7 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel, this.isBigEndian); + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs index 0fe1b60ecf..2e89396075 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -15,16 +16,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { private readonly bool isBigEndian; + private readonly TiffColorType colorType; + /// /// Initializes a new instance of the class. /// /// The memoryAllocator to use for buffer allocations. /// The image width. /// The bits used per pixel. + /// The color type of the pixel data. /// The tiff predictor used. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor, bool isBigEndian) - : base(memoryAllocator, width, bitsPerPixel, predictor) => this.isBigEndian = isBigEndian; + public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + this.colorType = colorType; + this.isBigEndian = isBigEndian; + } /// protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) @@ -34,7 +42,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel, this.isBigEndian); + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index da84b51b39..3685167d53 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.PixelFormats; @@ -20,21 +21,28 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// Buffer with decompressed pixel data. /// The width of the image or strip. - /// Bits per pixel. + /// The color type of the pixel data. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public static void Undo(Span pixelBytes, int width, int bitsPerPixel, bool isBigEndian) + public static void Undo(Span pixelBytes, int width, TiffColorType colorType, bool isBigEndian) { - if (bitsPerPixel == 8) - { - Undo8Bit(pixelBytes, width); - } - else if (bitsPerPixel == 16) + switch (colorType) { - Undo16Bit(pixelBytes, width, isBigEndian); - } - else if (bitsPerPixel == 24) - { - Undo24Bit(pixelBytes, width); + case TiffColorType.BlackIsZero8: + case TiffColorType.WhiteIsZero8: + case TiffColorType.PaletteColor: + UndoGray8Bit(pixelBytes, width); + break; + case TiffColorType.BlackIsZero16: + case TiffColorType.WhiteIsZero16: + UndoGray16Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.BlackIsZero32: + case TiffColorType.WhiteIsZero32: + UndoGray32Bit(pixelBytes, width, isBigEndian); + break; + case TiffColorType.Rgb888: + UndoRgb24Bit(pixelBytes, width); + break; } } @@ -99,7 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } - private static void Undo8Bit(Span pixelBytes, int width) + private static void UndoGray8Bit(Span pixelBytes, int width) { int rowBytesCount = width; int height = pixelBytes.Length / rowBytesCount; @@ -116,7 +124,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } - private static void Undo16Bit(Span pixelBytes, int width, bool isBigEndian) + private static void UndoGray16Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 2; int height = pixelBytes.Length / rowBytesCount; @@ -160,7 +168,51 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } - private static void Undo24Bit(Span pixelBytes, int width) + private static void UndoGray32Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtils.ConvertToUIntBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, pixelValue); + offset += 4; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, pixelValue); + offset += 4; + } + } + } + } + + private static void UndoRgb24Bit(Span pixelBytes, int width) { int rowBytesCount = width * 3; int height = pixelBytes.Length / rowBytesCount; diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index 964f0bf127..083e53950a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Tiff.Compression @@ -15,6 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression TiffPhotometricInterpretation photometricInterpretation, int width, int bitsPerPixel, + TiffColorType colorType, TiffPredictor predictor, FaxCompressionOptions faxOptions, ByteOrder byteOrder) @@ -33,11 +35,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffDecoderCompressionType.Deflate: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new DeflateTiffCompression(allocator, width, bitsPerPixel, predictor, byteOrder == ByteOrder.BigEndian); + return new DeflateTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian); case TiffDecoderCompressionType.Lzw: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new LzwTiffCompression(allocator, width, bitsPerPixel, predictor, byteOrder == ByteOrder.BigEndian); + return new LzwTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian); case TiffDecoderCompressionType.T4: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs index 862756bc42..f54a794840 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // 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; diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 2c7e8d0c8f..37bc01ad18 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -270,6 +270,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.PhotometricInterpretation, frame.Width, bitsPerPixel, + this.ColorType, this.Predictor, this.FaxCompressionOptions, this.byteOrder); @@ -321,6 +322,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.PhotometricInterpretation, frame.Width, bitsPerPixel, + this.ColorType, this.Predictor, this.FaxCompressionOptions, this.byteOrder); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs index e9399e23b5..c93a2018df 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -5,6 +5,7 @@ using System.IO; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; using Xunit; @@ -26,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression { var buffer = new byte[data.Length]; - using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None, false); + using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index 834d206cc7..5ea75d9a84 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -5,6 +5,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors; using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; using Xunit; @@ -38,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression using BufferedReadStream stream = CreateCompressedStream(data); var buffer = new byte[data.Length]; - using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None, false); + using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); Assert.Equal(data, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index bba27039dd..a8460e6c93 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -197,6 +197,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(Flower32BitGrayPredictorBigEndian, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayPredictorLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_32Bit_Gray_WithPredictor(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) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 08b0b2901f..ff6c198c4d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -607,6 +607,8 @@ namespace SixLabors.ImageSharp.Tests 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 Flower32BitGrayPredictorBigEndian = "Tiff/flower-minisblack-32_msb_deflate_predictor.tiff"; + public const string Flower32BitGrayPredictorLittleEndian = "Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff b/tests/Images/Input/Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff new file mode 100644 index 0000000000..2dc9e8092a --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e7998cc985ef11ab9da410f18dcfb6b9a3169fb1ec01f9e61aa38d8ee4cfb6 +size 12704 diff --git a/tests/Images/Input/Tiff/flower-minisblack-32_msb_deflate_predictor.tiff b/tests/Images/Input/Tiff/flower-minisblack-32_msb_deflate_predictor.tiff new file mode 100644 index 0000000000..f87c74c721 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32_msb_deflate_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630e7f46655b6e61c4de7d56946a3a9225db68f776f9062ff2d5372547cc7c02 +size 12704 From 484ec85ba7fec5a38bbc4dc82ae1ed17a741f97f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 Aug 2021 21:14:30 +0200 Subject: [PATCH 15/31] Add support for horizontal predictor with 16 bit color tiff's --- .../Tiff/Compression/HorizontalPredictor.cs | 79 +++++++++++++++++++ .../Formats/Tiff/TiffDecoderTests.cs | 6 ++ tests/ImageSharp.Tests/TestImages.cs | 2 + ...lower-rgb-contig-16_lsb_zip_predictor.tiff | 3 + ...lower-rgb-contig-16_msb_zip_predictor.tiff | 3 + 5 files changed, 93 insertions(+) create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-16_lsb_zip_predictor.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-16_msb_zip_predictor.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index 3685167d53..b4941c5396 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -43,6 +43,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffColorType.Rgb888: UndoRgb24Bit(pixelBytes, width); break; + case TiffColorType.Rgb161616: + UndoRgb48Bit(pixelBytes, width, isBigEndian); + break; } } @@ -236,5 +239,81 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } } + + private static void UndoRgb48Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; + } + } + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index a8460e6c93..f07924791e 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -228,6 +228,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_48Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb161616PredictorBigEndian, PixelTypes.Rgba32)] + [WithFile(FlowerRgb161616PredictorLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_48Bit_WithPredictor(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb242424ContiguousLittleEndian, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ff6c198c4d..1215574c57 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -573,6 +573,8 @@ namespace SixLabors.ImageSharp.Tests 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 FlowerRgb161616PredictorBigEndian = "Tiff/flower-rgb-contig-16_msb_zip_predictor.tiff"; + public const string FlowerRgb161616PredictorLittleEndian = "Tiff/flower-rgb-contig-16_lsb_zip_predictor.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"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb_zip_predictor.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb_zip_predictor.tiff new file mode 100644 index 0000000000..69fe133f8c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb_zip_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb465c21009def345d192319ba13ba2e1e537310eec3ab2cce680f0d111a4f18 +size 18256 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-16_msb_zip_predictor.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-16_msb_zip_predictor.tiff new file mode 100644 index 0000000000..231de2eaad --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-16_msb_zip_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c098beb9c8f250e9d4f6eb66a3a42f3852ad3ca86aabbdbacfa897d93ec8bc0d +size 18254 From f8f0c101c87dc76ed63ee77ce9ce410da234fbfe Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 Aug 2021 21:28:39 +0200 Subject: [PATCH 16/31] Add support for horizontal predictor with 32 bit color tiff's --- .../Tiff/Compression/HorizontalPredictor.cs | 79 +++++++++++++++++++ .../Formats/Tiff/TiffDecoderTests.cs | 11 +++ tests/ImageSharp.Tests/TestImages.cs | 2 + ...lower-rgb-contig-32_lsb_zip_predictor.tiff | 3 + ...lower-rgb-contig-32_msb_zip_predictor.tiff | 3 + 5 files changed, 98 insertions(+) create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-32_lsb_zip_predictor.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-32_msb_zip_predictor.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index b4941c5396..e2dbc6ca99 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -46,6 +46,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffColorType.Rgb161616: UndoRgb48Bit(pixelBytes, width, isBigEndian); break; + case TiffColorType.Rgb323232: + UndoRgb96Bit(pixelBytes, width, isBigEndian); + break; } } @@ -315,5 +318,81 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } } + + private static void UndoRgb96Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 12; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + } + } + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index f07924791e..5c235dfcf8 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -260,6 +260,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(FlowerRgb323232PredictorBigEndian, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232PredictorLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_96Bit_WithPredictor(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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1215574c57..bde2de5b88 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -567,6 +567,8 @@ namespace SixLabors.ImageSharp.Tests 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 FlowerRgb323232PredictorBigEndian = "Tiff/flower-rgb-contig-32_msb_zip_predictor.tiff"; + public const string FlowerRgb323232PredictorLittleEndian = "Tiff/flower-rgb-contig-32_lsb_zip_predictor.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"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb_zip_predictor.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb_zip_predictor.tiff new file mode 100644 index 0000000000..ca3fee9da5 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb_zip_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ed60ad91ea70db01789f9dd37745d8a5ab86f72b98637cf2007b4e28a71976f +size 37632 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32_msb_zip_predictor.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32_msb_zip_predictor.tiff new file mode 100644 index 0000000000..07a1ae74f7 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32_msb_zip_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1086c2ec568c5c3c2b08fcc66691fab017eb6fad109373a1dadd2e12fae86bc8 +size 37630 From 8b469b4368a3b6b5f6430558d70d4bb58300c183 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 16:34:21 +0200 Subject: [PATCH 17/31] Add support for decoding ycbcr tiff's --- .../TiffColorDecoderFactory{TPixel}.cs | 11 +- .../TiffColorType.cs | 2 + .../YCbCrTiffColor{TPixel}.cs | 138 ++++++++++++++++++ .../Formats/Tiff/TiffDecoderCore.cs | 12 +- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 14 ++ src/ImageSharp/Primitives/Rational.cs | 45 ++---- .../Formats/Tiff/TiffDecoderTests.cs | 10 ++ tests/ImageSharp.Tests/TestImages.cs | 8 +- .../Input/Tiff/flower-rgb-contig-08.tiff | 3 + .../Input/Tiff/flower_ycbcr_contig-08.tiff | 3 + 10 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-08.tiff create mode 100644 tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 0c93998c48..010897c3c1 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -8,7 +8,13 @@ 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( + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ByteOrder byteOrder) { switch (colorType) { @@ -140,6 +146,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); + case TiffColorType.YCbCr: + return new YCbCrTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + 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 331065d273..4e108f58e3 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -107,5 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RGB Full Color. Planar configuration of data. /// RgbPlanar, + + YCbCr } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs new file mode 100644 index 0000000000..18a85f4e81 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + { + new Rational(299, 1000), + new Rational(587, 1000), + new Rational(114, 1000) + }; + + private static readonly Rational[] DefaultReferenceBlackWhite = + { + new Rational(0, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1) + }; + + public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset += 3; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Math.Min(Math.Max(input, 0), 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + var pixel = default(Rgba32); + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 011d037796..1693ed4528 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,6 +75,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffColorType ColorType { get; set; } + public Rational[] ReferenceBlackAndWhite { get; set; } + + public Rational[] YcbcrCoefficients { get; set; } + /// /// Gets or sets the compression used, when the image was encoded. /// @@ -316,7 +320,13 @@ 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.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + 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 14c527a34c..8ab543b500 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -69,6 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); + options.ReferenceBlackAndWhite = exifProfile.GetValue(ExifTag.ReferenceBlackWhite)?.Value; + options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); @@ -264,6 +266,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffPhotometricInterpretation.YCbCr: + { + options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value; + if (options.BitsPerSample.Channels != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + options.ColorType = TiffColorType.YCbCr; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); diff --git a/src/ImageSharp/Primitives/Rational.cs b/src/ImageSharp/Primitives/Rational.cs index b6f83e277d..b14681c696 100644 --- a/src/ImageSharp/Primitives/Rational.cs +++ b/src/ImageSharp/Primitives/Rational.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp { if (simplify) { - var rational = new LongRational(numerator, denominator).Simplify(); + LongRational rational = new LongRational(numerator, denominator).Simplify(); this.Numerator = (uint)rational.Numerator; this.Denominator = (uint)rational.Denominator; @@ -93,10 +93,7 @@ namespace SixLabors.ImageSharp /// The first to compare. /// The second to compare. /// The - public static bool operator ==(Rational left, Rational right) - { - return left.Equals(right); - } + public static bool operator ==(Rational left, Rational right) => left.Equals(right); /// /// Determines whether the specified instances are not considered equal. @@ -104,10 +101,7 @@ namespace SixLabors.ImageSharp /// The first to compare. /// The second to compare. /// The - public static bool operator !=(Rational left, Rational right) - { - return !left.Equals(right); - } + public static bool operator !=(Rational left, Rational right) => !left.Equals(right); /// /// Converts the specified to an instance of this type. @@ -116,10 +110,7 @@ namespace SixLabors.ImageSharp /// /// The . /// - public static Rational FromDouble(double value) - { - return new Rational(value, false); - } + public static Rational FromDouble(double value) => new Rational(value, false); /// /// Converts the specified to an instance of this type. @@ -129,16 +120,10 @@ namespace SixLabors.ImageSharp /// /// The . /// - public static Rational FromDouble(double value, bool bestPrecision) - { - return new Rational(value, bestPrecision); - } + public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision); /// - public override bool Equals(object obj) - { - return obj is Rational other && this.Equals(other); - } + public override bool Equals(object obj) => obj is Rational other && this.Equals(other); /// public bool Equals(Rational other) @@ -162,16 +147,18 @@ namespace SixLabors.ImageSharp /// /// The . /// - public double ToDouble() - { - return this.Numerator / (double)this.Denominator; - } + public double ToDouble() => this.Numerator / (double)this.Denominator; + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public float ToSingle() => this.Numerator / (float)this.Denominator; /// - public override string ToString() - { - return this.ToString(CultureInfo.InvariantCulture); - } + public override string ToString() => this.ToString(CultureInfo.InvariantCulture); /// /// Converts the numeric value of this instance to its equivalent string representation using diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 67892b14b1..4b7438af4c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -153,6 +153,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb888Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 929e375246..ce3e7ecb3c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -569,15 +569,17 @@ namespace SixLabors.ImageSharp.Tests 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 FlowerRgb121212Contiguous = "Tiff/flower-rgb-contig-12.tiff"; public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff"; public const string FlowerRgb101010Planar = "Tiff/flower-rgb-planar-10.tiff"; - public const string FlowerRgb121212Contiguous = "Tiff/flower-rgb-contig-12.tiff"; + public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.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 FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; 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"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff new file mode 100644 index 0000000000..53e890e3ca --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd1333eb93d8e7ea614b755ca1c8909c67b4b44fc03a8cab6be5491bf4d15841 +size 9753 diff --git a/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff b/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff new file mode 100644 index 0000000000..bc7f2178b1 --- /dev/null +++ b/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95f7b71c3a333734f799d73076032e31a6dfff1802bb3b454ba1eada7be50b0d +size 10058 From da8f14d97f1572916c64343bea798e178e0e0fac Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 17:29:38 +0200 Subject: [PATCH 18/31] Add support for decoding ycbcr tiff's with planar configuration --- .../TiffColorDecoderFactory{TPixel}.cs | 11 +- .../TiffColorType.cs | 10 +- .../YCbCrConverter.cs | 121 ++++++++++++++++++ .../YCbCrPlanarTiffColor{TPixel}.cs | 40 ++++++ .../YCbCrTiffColor{TPixel}.cs | 79 +----------- .../Formats/Tiff/TiffDecoderCore.cs | 19 ++- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 3 +- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-ycbcr-planar-08.tiff | 3 + 9 files changed, 205 insertions(+), 82 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 010897c3c1..d279712746 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -154,7 +154,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } - public static TiffBasePlanarColorDecoder CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBasePlanarColorDecoder CreatePlanar( + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ByteOrder byteOrder) { switch (colorType) { @@ -167,6 +173,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.YCbCrPlanar: + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + 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 4e108f58e3..51c9be83f0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -108,6 +108,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RgbPlanar, - YCbCr + /// + /// The pixels are stored in YCbCr format. + /// + YCbCr, + + /// + /// The pixels are stored in YCbCr format as planar. + /// + YCbCrPlanar } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs new file mode 100644 index 0000000000..3e28e30bc4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Converts YCbCr data to rgb data. + /// + internal class YCbCrConverter + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + { + new Rational(299, 1000), + new Rational(587, 1000), + new Rational(114, 1000) + }; + + private static readonly Rational[] DefaultReferenceBlackWhite = + { + new Rational(0, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1) + }; + + public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Math.Min(Math.Max(input, 0), 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + var pixel = default(Rgba32); + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000000..126033e318 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly YCbCrConverter converter; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span yData = data[0].GetSpan(); + Span cbData = data[1].GetSpan(); + Span crData = data[2].GetSpan(); + + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset++; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 18a85f4e81..55ad67a3cf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,10 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { - private readonly CodingRangeExpander yExpander; - private readonly CodingRangeExpander cbExpander; - private readonly CodingRangeExpander crExpander; - private readonly YCbCrToRgbConverter converter; + private readonly YCbCrConverter converter; private static readonly Rational[] DefaultLuma = { @@ -45,10 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); } - this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); - this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); - this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); - this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); } /// @@ -61,78 +54,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - Rgba32 rgba = this.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) - { - float yExpanded = this.yExpander.Expand(y); - float cbExpanded = this.cbExpander.Expand(cb); - float crExpanded = this.crExpander.Expand(cr); - - Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); - - return rgba; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte RoundAndClampTo8Bit(float value) - { - int input = (int)MathF.Round(value); - return (byte)Math.Min(Math.Max(input, 0), 255); - } - - private readonly struct CodingRangeExpander - { - private readonly float f1; - private readonly float f2; - - public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) - { - float black = referenceBlack.ToSingle(); - float white = referenceWhite.ToSingle(); - this.f1 = codingRange / (white - black); - this.f2 = this.f1 * black; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Expand(float code) => (code * this.f1) - this.f2; - } - - private readonly struct YCbCrToRgbConverter - { - private readonly float cr2R; - private readonly float cb2B; - private readonly float y2G; - private readonly float cr2G; - private readonly float cb2G; - - public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) - { - this.cr2R = 2 - (2 * lumaRed.ToSingle()); - this.cb2B = 2 - (2 * lumaBlue.ToSingle()); - this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); - this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); - this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Rgba32 Convert(float y, float cb, float cr) - { - var pixel = default(Rgba32); - pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); - pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); - pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); - pixel.A = byte.MaxValue; - - return pixel; - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 1693ed4528..69b6c66157 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -268,9 +268,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frame.Width, + bitsPerPixel, + this.Predictor, + this.FaxCompressionOptions); + + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 8ab543b500..857509f87a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -274,7 +274,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } - options.ColorType = TiffColorType.YCbCr; + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; + break; } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ce3e7ecb3c..5c2d3ceb55 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -576,6 +576,7 @@ namespace SixLabors.ImageSharp.Tests 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 FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower_ycbcr_planar-08.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; diff --git a/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff new file mode 100644 index 0000000000..4506ff3e93 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc4d8033214a6737f41c4e32d9314db77b3b1ae14515496f10468047390f6c5 +size 10042 From 1ec339458958cffc226a2089e9f4ad622505d785 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 19:30:01 +0200 Subject: [PATCH 19/31] Throw NotSupported exception when luma and chroma subsampling is not equal --- .../YCbCrTiffColor{TPixel}.cs | 32 +------------------ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 12 +++++++ .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 4 +-- ...ig-08.tiff => flower-ycbcr-contig-08.tiff} | 0 5 files changed, 16 insertions(+), 33 deletions(-) rename tests/Images/Input/Tiff/{flower_ycbcr_contig-08.tiff => flower-ycbcr-contig-08.tiff} (100%) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 55ad67a3cf..0678272124 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -12,37 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly YCbCrConverter converter; - private static readonly Rational[] DefaultLuma = - { - new Rational(299, 1000), - new Rational(587, 1000), - new Rational(114, 1000) - }; - - private static readonly Rational[] DefaultReferenceBlackWhite = - { - new Rational(0, 1), new Rational(255, 1), - new Rational(128, 1), new Rational(255, 1), - new Rational(128, 1), new Rational(255, 1) - }; - - public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) - { - referenceBlackAndWhite ??= DefaultReferenceBlackWhite; - coefficients ??= DefaultLuma; - - if (referenceBlackAndWhite.Length != 6) - { - TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); - } - - if (coefficients.Length != 3) - { - TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); - } - - this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); - } + public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 857509f87a..1d1473dc43 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -57,6 +57,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } + ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; + if (ycbcrSubSampling != null && ycbcrSubSampling[0] != ycbcrSubSampling[1]) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); + } + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) { TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); @@ -274,6 +280,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel != 8) + { + TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for YCbCr images."); + } + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; break; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 4b7438af4c..b2e2e2535d 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -160,6 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5c2d3ceb55..a79ff9da84 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -575,8 +575,8 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.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 FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.tiff"; - public const string FlowerYCbCr888Planar = "Tiff/flower_ycbcr_planar-08.tiff"; + public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; diff --git a/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff From 1cbab324681f3b5c6ed1085fd45a75c0dd5ae6ae Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 20:25:54 +0200 Subject: [PATCH 20/31] Throw not suported for YCbCr with subsampling --- src/ImageSharp/Formats/Tiff/README.md | 8 ++++---- src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 5b116b819a..3e4afe92e8 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -63,7 +63,7 @@ |PaletteColor | Y | Y | General implementation only | |TransparencyMask | | | | |Separated (TIFF Extension) | | | | -|YCbCr (TIFF Extension) | | | | +|YCbCr (TIFF Extension) | | Y | | |CieLab (TIFF Extension) | | | | |IccLab (TechNote 1) | | | | @@ -165,10 +165,10 @@ |JPEGQTables | | | | |JPEGDCTables | | | | |JPEGACTables | | | | -|YCbCrCoefficients | | | | -|YCbCrSubSampling | | | | +|YCbCrCoefficients | | Y | | +|YCbCrSubSampling | | Y | | |YCbCrPositioning | | | | -|ReferenceBlackWhite | | | | +|ReferenceBlackWhite | | Y | | |StripRowCounts | - | - | See RFC 2301 (File Format for Internet Fax). | |XMP | Y | Y | | |ImageID | | | | diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 1d1473dc43..86dc3c1df6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -63,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); } + if (ycbcrSubSampling != null && ycbcrSubSampling[0] != 1) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images without subsampling."); + } + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) { TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); From 617a66d120c9d4e74611b7c05b534738fa210def Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 11:32:10 +0200 Subject: [PATCH 21/31] Reverse chroma sub sampling --- .../TiffColorDecoderFactory{TPixel}.cs | 8 ++- .../YCbCrPlanarTiffColor{TPixel}.cs | 27 +++++++++- .../YCbCrTiffColor{TPixel}.cs | 53 ++++++++++++++++++- .../Formats/Tiff/TiffDecoderCore.cs | 14 +++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 9 ++-- .../Formats/Tiff/TiffDecoderTests.cs | 12 ++++- tests/ImageSharp.Tests/TestImages.cs | 4 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff | 3 ++ 12 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index d279712746..13f95fedf8 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation @@ -9,11 +10,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation where TPixel : unmanaged, IPixel { public static TiffBaseColorDecoder Create( + MemoryAllocator memoryAllocator, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -147,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new PaletteTiffColor(bitsPerSample, colorMap); case TiffColorType.YCbCr: - return new YCbCrTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); @@ -160,6 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -174,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); case TiffColorType.YCbCrPlanar: - return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 126033e318..0e411c69db 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -13,7 +13,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly YCbCrConverter converter; - public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) @@ -22,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span cbData = data[1].GetSpan(); Span crData = data[2].GetSpan(); + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], cbData, crData); + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -36,5 +47,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) + { + for (int row = height - 1; row >= 0; row--) + { + for (int col = width - 1; col >= 0; col--) + { + int offset = (row * width) + col; + int subSampleOffset = (row / verticalSubSampling * (width / horizontalSubSampling)) + (col / horizontalSubSampling); + planarCb[offset] = planarCb[subSampleOffset]; + planarCr[offset] = planarCr[subSampleOffset]; + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 0678272124..7a7e1ad879 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,13 +11,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly MemoryAllocator memoryAllocator; + private readonly YCbCrConverter converter; - public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrTiffColor(MemoryAllocator memoryAllocator, Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.memoryAllocator = memoryAllocator; + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + ReadOnlySpan ycbcrData = data; + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(data.Length); + Span tmpBufferSpan = tmpBuffer.GetSpan(); + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); + ycbcrData = tmpBufferSpan; + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -24,12 +43,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) + { + int blockWidth = width / horizontalSubSampling; + int blockHeight = height / verticalSubSampling; + int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; + int blockByteCount = cbCrOffsetInBlock + 2; + + for (int blockRow = blockHeight - 1; blockRow >= 0; blockRow--) + { + for (int blockCol = blockWidth - 1; blockCol >= 0; blockCol--) + { + int blockOffset = (blockRow * blockWidth) + blockCol; + ReadOnlySpan blockData = source.Slice(blockOffset * blockByteCount, blockByteCount); + byte cr = blockData[cbCrOffsetInBlock + 1]; + byte cb = blockData[cbCrOffsetInBlock]; + + for (int row = verticalSubSampling - 1; row >= 0; row--) + { + for (int col = horizontalSubSampling - 1; col >= 0; col--) + { + int offset = 3 * ((((blockRow * verticalSubSampling) + row) * width) + (blockCol * horizontalSubSampling) + col); + destination[offset + 2] = cr; + destination[offset + 1] = cb; + destination[offset] = blockData[(row * horizontalSubSampling) + col]; + } + } + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 69b6c66157..080cdb22a5 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,10 +75,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffColorType ColorType { get; set; } + /// + /// Gets or sets the reference black and white for decoding YCbCr pixel data. + /// public Rational[] ReferenceBlackAndWhite { get; set; } + /// + /// Gets or sets the YCbCr coefficients. + /// public Rational[] YcbcrCoefficients { get; set; } + /// + /// Gets or sets the YCbCr sub sampling. + /// + public ushort[] YcbcrSubSampling { get; set; } + /// /// Gets or sets the compression used, when the image was encoded. /// @@ -283,6 +294,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) @@ -334,11 +346,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.FaxCompressionOptions); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.memoryAllocator, this.ColorType, this.BitsPerSample, this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, 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 86dc3c1df6..15764cffa0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -58,14 +58,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != ycbcrSubSampling[1]) + if (ycbcrSubSampling != null && ycbcrSubSampling.Length != 2) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); + TiffThrowHelper.ThrowImageFormatException("Invalid YCbCrSubsampling, expected 2 values."); } - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != 1) + if (ycbcrSubSampling != null && ycbcrSubSampling[1] > ycbcrSubSampling[0]) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images without subsampling."); + TiffThrowHelper.ThrowImageFormatException("ChromaSubsampleVert shall always be less than or equal to ChromaSubsampleHoriz."); } if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) @@ -82,6 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); options.ReferenceBlackAndWhite = exifProfile.GetValue(ExifTag.ReferenceBlackWhite)?.Value; options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; + options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index b2e2e2535d..18007d1c4b 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -161,8 +161,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush1v1, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong + // converting the pixel data from Magick.Net to our format with YCbCr? + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a79ff9da84..3ad30b5452 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -577,6 +577,10 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.tiff"; + public const string RgbYCbCr888Contiguoush1v1 = "Tiff/rgb-ycbcr-contig-08_h1v1.tiff"; + public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff"; + public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff"; + public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff new file mode 100644 index 0000000000..801cab5749 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:389ee18596cd3d9f1f7f04b4db8fd21edce2900837c17ebb57cc4b64a6820f3e +size 120354 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff new file mode 100644 index 0000000000..82350e5b29 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95b1ba4ff48ea2263041eca4ada44d009277297bb3b3a185d48580bdf3f7caaf +size 81382 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff new file mode 100644 index 0000000000..b282b1742a --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd835c2406700523b239b80299b2b02c36d41182ac338f7ed7164979a787c60 +size 63438 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff new file mode 100644 index 0000000000..0c8c048dcc --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:399d5bc062baa00c2054a138489709379032f8683fbcb292bb2125b62e715b5f +size 50336 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff new file mode 100644 index 0000000000..45341ed264 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58c4914b32b27df1ef303bb127fe9211c2aeda23e17bb5f4b349543c96d845b7 +size 45152 From 23c692656584b0481563250cddea33710245eb0f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 13:20:05 +0200 Subject: [PATCH 22/31] Add padding when width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert --- .../YCbCrTiffColor{TPixel}.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 7a7e1ad879..b6944168d4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -30,7 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ReadOnlySpan ycbcrData = data; if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) { - using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(data.Length); + // 4 extra rows and columns for possible padding. + int paddedWidth = width + 4; + int paddedHeight = height + 4; + int requiredBytes = paddedWidth * paddedHeight * 3; + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(requiredBytes); Span tmpBufferSpan = tmpBuffer.GetSpan(); ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); ycbcrData = tmpBufferSpan; @@ -53,6 +57,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += width % horizontalSubSampling; + height += height % verticalSubSampling; int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; From 5a2a28bfb87ede49fdf91555d45189aeb6d9a85d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 14:41:43 +0200 Subject: [PATCH 23/31] Add padding to width to next integer multiple of horizontalSubSampling --- .../YCbCrTiffColor{TPixel}.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index b6944168d4..f28a2399e8 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -42,6 +42,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); @@ -52,6 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation pixelRow[x] = color; offset += 3; } + + offset += widthPadding * 3; } } @@ -59,8 +68,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, // then the source data will be padded. - width += width % horizontalSubSampling; - height += height % verticalSubSampling; + width += PaddingToNextInteger(width, horizontalSubSampling); + height += PaddingToNextInteger(height, verticalSubSampling); int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; @@ -88,5 +97,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } + + private static int PaddingToNextInteger(int valueToRoundUp, int subSampling) + { + if (valueToRoundUp % subSampling == 0) + { + return 0; + } + + int padding = subSampling - (valueToRoundUp % subSampling); + return padding; + } } } From b576f15b13fe1c57f5a7aed1f1e45025cc4f8b1b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 15:02:30 +0200 Subject: [PATCH 24/31] Additional test images --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 3 +++ tests/ImageSharp.Tests/TestImages.cs | 7 +++++-- ...bcr-contig-08.tiff => flower-ycbcr-contig-08_h1v1.tiff} | 0 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff | 3 +++ tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff | 3 +++ tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff | 3 +++ ...bcr-planar-08.tiff => flower-ycbcr-planar-08_h1v1.tiff} | 0 7 files changed, 17 insertions(+), 2 deletions(-) rename tests/Images/Input/Tiff/{flower-ycbcr-contig-08.tiff => flower-ycbcr-contig-08_h1v1.tiff} (100%) create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff rename tests/Images/Input/Tiff/{flower-ycbcr-planar-08.tiff => flower-ycbcr-planar-08_h1v1.tiff} (100%) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index d0fed1e208..b52c02d26c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -179,6 +179,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] [WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] [WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 65b9bfe440..db258683cc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -585,8 +585,11 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.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 FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; - public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.tiff"; + public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08_h1v1.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08_h1v1.tiff"; + public const string FlowerYCbCr888Contiguoush2v1 = "Tiff/flower-ycbcr-contig-08_h2v1.tiff"; + public const string FlowerYCbCr888Contiguoush2v2 = "Tiff/flower-ycbcr-contig-08_h2v2.tiff"; + public const string FlowerYCbCr888Contiguoush4v4 = "Tiff/flower-ycbcr-contig-08_h4v4.tiff"; public const string RgbYCbCr888Contiguoush1v1 = "Tiff/rgb-ycbcr-contig-08_h1v1.tiff"; public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff"; public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff"; diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff new file mode 100644 index 0000000000..f5133b9f3c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:270e0331818a755f5fac600172eacbcbebda86f93f521bfc8d75f4b8bc530177 +size 6944 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff new file mode 100644 index 0000000000..98fb13d66a --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef6ebc9dfe72fbe6ed65ebfc2465ebb18f326119a640faf3301aa4cfa31990f +size 5464 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff new file mode 100644 index 0000000000..79aace2a32 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ea966cc7b823a5d228b49cdc55a261353f73b1eb94a218f1c68321d757e25f +size 4342 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff From 2789f6a6a7a430d7cbae94c5e3f9ca7d822ed2cb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 16:42:51 +0200 Subject: [PATCH 25/31] Add width padding for planar ycbcr --- .../YCbCrPlanarTiffColor{TPixel}.cs | 15 +++++++++++++++ .../YCbCrTiffColor{TPixel}.cs | 18 ++++-------------- src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 0e411c69db..70578a7442 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -35,6 +36,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); @@ -45,11 +53,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation pixelRow[x] = color; offset++; } + + offset += widthPadding; } } private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling); + for (int row = height - 1; row >= 0; row--) { for (int col = width - 1; col >= 0; col--) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index f28a2399e8..e31b4984d3 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation if (this.ycbcrSubSampling != null) { // Round to the next integer multiple of horizontalSubSampling. - widthPadding = PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); } for (int y = top; y < top + height; y++) @@ -68,8 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, // then the source data will be padded. - width += PaddingToNextInteger(width, horizontalSubSampling); - height += PaddingToNextInteger(height, verticalSubSampling); + width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling); int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; @@ -97,16 +98,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } - - private static int PaddingToNextInteger(int valueToRoundUp, int subSampling) - { - if (valueToRoundUp % subSampling == 0) - { - return 0; - } - - int padding = subSampling - (valueToRoundUp % subSampling); - return padding; - } } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index f2872858cf..4f71fa35c9 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -98,5 +98,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils color.FromVector4(colorVector); return color; } + + /// + /// Finds the padding needed to round 'valueToRoundUp' to the next integer multiple of subSampling value. + /// + /// The width or height to round up. + /// The sub sampling. + /// The padding. + public static int PaddingToNextInteger(int valueToRoundUp, int subSampling) + { + if (valueToRoundUp % subSampling == 0) + { + return 0; + } + + int padding = subSampling - (valueToRoundUp % subSampling); + return padding; + } } } From 59b470f4daf85e1554eaf1ffb2faea7b7b59811a Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:05:01 +0200 Subject: [PATCH 26/31] Use Math.Clamp(input, 0, 255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs index 3e28e30bc4..aa64a6e32e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static byte RoundAndClampTo8Bit(float value) { int input = (int)MathF.Round(value); - return (byte)Math.Min(Math.Max(input, 0), 255); + return (byte)Math.Clamp(input, 0, 255); } private readonly struct CodingRangeExpander From d6bbdadeaef22c5a8dcf1be66679e91f177d658d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 17:12:25 +0200 Subject: [PATCH 27/31] Use Numerics.Clamp instead Math.Clamp (not available with net472) --- .../Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs index aa64a6e32e..c6594f9084 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static byte RoundAndClampTo8Bit(float value) { int input = (int)MathF.Round(value); - return (byte)Math.Clamp(input, 0, 255); + return (byte)Numerics.Clamp(input, 0, 255); } private readonly struct CodingRangeExpander From 18edc46b0fee295151671e1e67230bf441b59827 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 Aug 2021 00:21:01 +0200 Subject: [PATCH 28/31] If component id's are R, G, B in ASCII the color space should be RGB --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 896e5f0aaf..269b2fe76b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -345,10 +345,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Returns the correct colorspace based on the image component count + /// Returns the correct colorspace based on the image component count and the jpeg frame components. /// /// The - private JpegColorSpace DeduceJpegColorSpace(byte componentCount) + private JpegColorSpace DeduceJpegColorSpace(byte componentCount, JpegComponent[] components) { if (componentCount == 1) { @@ -362,6 +362,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return JpegColorSpace.RGB; } + // If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr. + if (components[0].Id == 82 && components[1].Id == 71 && components[2].Id == 66) + { + return JpegColorSpace.RGB; + } + // Some images are poorly encoded and contain incorrect colorspace transform metadata. // We ignore that and always fall back to the default colorspace. return JpegColorSpace.YCbCr; @@ -836,9 +842,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // 1 byte: Number of components byte componentCount = this.temp[5]; - this.ColorSpace = this.DeduceJpegColorSpace(componentCount); - - this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); @@ -888,6 +891,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg index += componentBytes; } + this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components); + this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + this.Frame.Init(maxH, maxV); this.scanDecoder.InjectFrameData(this.Frame, this); From 6cb871711762f9327b4deed88ab0e5a85fdfd5aa Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 Aug 2021 00:36:09 +0200 Subject: [PATCH 29/31] Add unit test for issue #1732 --- .../Formats/Jpg/JpegDecoderTests.cs | 13 +++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Jpg/issues/Issue1732-WrongColorSpace.jpg | 3 +++ 3 files changed, 17 insertions(+) create mode 100644 tests/Images/Input/Jpg/issues/Issue1732-WrongColorSpace.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 674aa6d8f6..e8d307f909 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -174,6 +174,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg await Assert.ThrowsAsync(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token)); } + // https://github.com/SixLabors/ImageSharp/pull/1732 + [Theory] + [WithFile(TestImages.Jpeg.Issues.WrongColorSpace, PixelTypes.Rgba32)] + public void Issue1732_DecodesWithRgbColorSpace(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new JpegDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 059e51cec9..8b9aa1a5ff 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -237,6 +237,7 @@ namespace SixLabors.ImageSharp.Tests public const string ExifResize1049 = "Jpg/issues/issue1049-exif-resize.jpg"; public const string BadSubSampling1076 = "Jpg/issues/issue-1076-invalid-subsampling.jpg"; public const string IdentifyMultiFrame1211 = "Jpg/issues/issue-1221-identify-multi-frame.jpg"; + public const string WrongColorSpace = "Jpg/issues/Issue1732-WrongColorSpace.jpg"; public static class Fuzz { diff --git a/tests/Images/Input/Jpg/issues/Issue1732-WrongColorSpace.jpg b/tests/Images/Input/Jpg/issues/Issue1732-WrongColorSpace.jpg new file mode 100644 index 0000000000..e3ba85ae83 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue1732-WrongColorSpace.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c72235954cdfb9d0cc7f09c537704e617313dc77708b4dca27b47c94c5e67a6 +size 2852 From 68a706fb06f576bcf2c0e1dfb038710b9f732753 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 Aug 2021 01:05:14 +0200 Subject: [PATCH 30/31] Move reading frame ComponentIds out of only metadata block --- .../Formats/Jpeg/JpegDecoderCore.cs | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 269b2fe76b..413c9b2bdb 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -845,57 +845,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); - if (!metadataOnly) + remaining -= length; + + // Validate: remaining part must be equal to components * 3 + const int componentBytes = 3; + if (remaining != componentCount * componentBytes) { - remaining -= length; + JpegThrowHelper.ThrowBadMarker("SOFn", remaining); + } - // Validate: remaining part must be equal to components * 3 - const int componentBytes = 3; - if (remaining != componentCount * componentBytes) - { - JpegThrowHelper.ThrowBadMarker("SOFn", remaining); - } + // components*3 bytes: component data + stream.Read(this.temp, 0, remaining); - // components*3 bytes: component data - stream.Read(this.temp, 0, remaining); + // No need to pool this. They max out at 4 + this.Frame.ComponentIds = new byte[componentCount]; + this.Frame.ComponentOrder = new byte[componentCount]; + this.Frame.Components = new JpegComponent[componentCount]; - // No need to pool this. They max out at 4 - this.Frame.ComponentIds = new byte[componentCount]; - this.Frame.ComponentOrder = new byte[componentCount]; - this.Frame.Components = new JpegComponent[componentCount]; + int maxH = 0; + int maxV = 0; + int index = 0; + for (int i = 0; i < componentCount; i++) + { + byte hv = this.temp[index + 1]; + int h = (hv >> 4) & 15; + int v = hv & 15; - int maxH = 0; - int maxV = 0; - int index = 0; - for (int i = 0; i < componentCount; i++) + if (maxH < h) { - byte hv = this.temp[index + 1]; - int h = (hv >> 4) & 15; - int v = hv & 15; + maxH = h; + } - if (maxH < h) - { - maxH = h; - } + if (maxV < v) + { + maxV = v; + } - if (maxV < v) - { - maxV = v; - } + var component = new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, this.temp[index], h, v, this.temp[index + 2], i); - var component = new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, this.temp[index], h, v, this.temp[index + 2], i); + this.Frame.Components[i] = component; + this.Frame.ComponentIds[i] = component.Id; - this.Frame.Components[i] = component; - this.Frame.ComponentIds[i] = component.Id; + index += componentBytes; + } - index += componentBytes; - } + this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components); - this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components); - this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + if (!metadataOnly) + { this.Frame.Init(maxH, maxV); - this.scanDecoder.InjectFrameData(this.Frame, this); } } From 7e7dbbb94317efa449a270de47d67879528feb72 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 13 Aug 2021 16:15:23 +0200 Subject: [PATCH 31/31] Switch order of component id check --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 413c9b2bdb..e94b07faae 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -363,7 +363,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr. - if (components[0].Id == 82 && components[1].Id == 71 && components[2].Id == 66) + if (components[2].Id == 66 && components[1].Id == 71 && components[0].Id == 82) { return JpegColorSpace.RGB; }