From 738f62bf9cc32c25d48876b3f629e624eb019dbe Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Sat, 1 Apr 2017 15:58:48 +0100 Subject: [PATCH] Implement image decoding for the most basic TIFF image format. --- .../Tiff/Compression/NoneTiffCompression.cs | 28 +++ .../Tiff/Compression/TiffCompressionType.cs | 18 ++ .../TiffColorType.cs | 18 ++ .../WhiteIsZero8TiffColor.cs | 45 ++++ .../Formats/Tiff/TiffDecoderCore.cs | 226 +++++++++++++++--- .../Formats/Tiff/Utils/TiffUtils.cs | 38 +++ .../Compression/NoneTiffCompressionTests.cs | 28 +++ .../PhotometricInterpretationTestBase.cs | 59 +++++ .../WhiteIsZero8TiffColorTests.cs | 51 ++++ .../Formats/Tiff/TiffDecoderImageTests.cs | 190 ++++++++++++++- .../Tiff/TiffGenIfdExtensions.cs | 2 +- 11 files changed, 660 insertions(+), 43 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs create mode 100644 src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs create mode 100644 src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs create mode 100644 tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs create mode 100644 tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs create mode 100644 tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs new file mode 100644 index 000000000..c538cf473 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Class to handle cases where TIFF image data is not compressed. + /// + internal static class NoneTiffCompression + { + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The number of bytes to read from the input stream. + /// The output buffer for uncompressed data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decompress(Stream stream, int byteCount, byte[] buffer) + { + stream.ReadFull(buffer, byteCount); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs new file mode 100644 index 000000000..5b6368bf9 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Provides enumeration of the various TIFF compression types. + /// + internal enum TiffCompressionType + { + /// + /// Image data is stored uncompressed in the TIFF file. + /// + None = 0 + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs new file mode 100644 index 000000000..bca27e4b2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Provides enumeration of the various TIFF photometric interpretation implementation types. + /// + internal enum TiffColorType + { + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for 8-bit images. + /// + WhiteIsZero8 + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs new file mode 100644 index 000000000..295db5e18 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + using System.Runtime.CompilerServices; + using ImageSharp; + + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimised for 8-bit grayscale images). + /// + internal static class WhiteIsZero8TiffColor + { + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The pixel format. + /// The buffer to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decode(byte[] data, PixelAccessor pixels, int left, int top, int width, int height) + where TColor : struct, IPixel + { + TColor color = default(TColor); + + uint offset = 0; + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + byte intensity = (byte)(255 - data[offset++]); + color.PackFromBytes(intensity, intensity, intensity, 255); + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 28d45a6e1..f186e33ee 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -6,6 +6,7 @@ namespace ImageSharp.Formats { using System; + using System.Buffers; using System.IO; using System.Text; @@ -41,6 +42,16 @@ namespace ImageSharp.Formats this.IsLittleEndian = isLittleEndian; } + /// + /// Gets or sets the photometric interpretation implementation to use when decoding the image. + /// + public TiffColorType ColorType { get; set; } + + /// + /// Gets or sets the compression implementation to use when decoding the image. + /// + public TiffCompressionType CompressionType { get; set; } + /// /// Gets the input stream. /// @@ -93,7 +104,7 @@ namespace ImageSharp.Formats public uint ReadHeader() { byte[] headerBytes = new byte[TiffConstants.SizeOfTiffHeader]; - this.ReadBytes(headerBytes, TiffConstants.SizeOfTiffHeader); + this.InputStream.ReadFull(headerBytes, TiffConstants.SizeOfTiffHeader); if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { @@ -129,13 +140,13 @@ namespace ImageSharp.Formats byte[] buffer = new byte[TiffConstants.SizeOfIfdEntry]; - this.ReadBytes(buffer, 2); + this.InputStream.ReadFull(buffer, 2); ushort entryCount = this.ToUInt16(buffer, 0); TiffIfdEntry[] entries = new TiffIfdEntry[entryCount]; for (int i = 0; i < entryCount; i++) { - this.ReadBytes(buffer, TiffConstants.SizeOfIfdEntry); + this.InputStream.ReadFull(buffer, TiffConstants.SizeOfIfdEntry); ushort tag = this.ToUInt16(buffer, 0); TiffType type = (TiffType)this.ToUInt16(buffer, 2); @@ -145,7 +156,7 @@ namespace ImageSharp.Formats entries[i] = new TiffIfdEntry(tag, type, count, value); } - this.ReadBytes(buffer, 4); + this.InputStream.ReadFull(buffer, 4); uint nextIfdOffset = this.ToUInt32(buffer, 0); return new TiffIfd(entries, nextIfdOffset); @@ -177,18 +188,184 @@ namespace ImageSharp.Formats resolutionUnit = (TiffResolutionUnit)this.ReadUnsignedInteger(ref resolutionUnitEntry); } - double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 1.0 / 2.54 : 1.0; + if (resolutionUnit != TiffResolutionUnit.None) + { + double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; + + if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry)) + { + Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry); + image.MetaData.HorizontalResolution = xResolution.ToDouble() * resolutionUnitFactor; + } + + if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry)) + { + Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry); + image.MetaData.VerticalResolution = yResolution.ToDouble() * resolutionUnitFactor; + } + } + + this.ReadImageFormat(ifd); + + if (ifd.TryGetIfdEntry(TiffTags.RowsPerStrip, out TiffIfdEntry rowsPerStripEntry) + && ifd.TryGetIfdEntry(TiffTags.StripOffsets, out TiffIfdEntry stripOffsetsEntry) + && ifd.TryGetIfdEntry(TiffTags.StripByteCounts, out TiffIfdEntry stripByteCountsEntry)) + { + int rowsPerStrip = (int)this.ReadUnsignedInteger(ref rowsPerStripEntry); + uint[] stripOffsets = this.ReadUnsignedIntegerArray(ref stripOffsetsEntry); + uint[] stripByteCounts = this.ReadUnsignedIntegerArray(ref stripByteCountsEntry); + + int uncompressedStripSize = this.CalculateImageBufferSize(width, rowsPerStrip); + + using (PixelAccessor pixels = image.Lock()) + { + byte[] stripBytes = ArrayPool.Shared.Rent(uncompressedStripSize); + + try + { + this.DecompressImageBlock(stripOffsets[0], stripByteCounts[0], stripBytes); + this.ProcessImageBlock(stripBytes, pixels, 0, 0, width, rowsPerStrip); + } + finally + { + ArrayPool.Shared.Return(stripBytes); + } + } + } + } + + /// + /// Determines the TIFF compression and color types, and reads any associated parameters. + /// + /// The IFD to read the image format information for. + public void ReadImageFormat(TiffIfd ifd) + { + TiffCompression compression = TiffCompression.None; - if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry)) + if (ifd.TryGetIfdEntry(TiffTags.Compression, out TiffIfdEntry compressionEntry)) { - Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry); - image.MetaData.HorizontalResolution = xResolution.ToDouble(); + compression = (TiffCompression)this.ReadUnsignedInteger(ref compressionEntry); } - - if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry)) + + switch (compression) { - Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry); - image.MetaData.VerticalResolution = yResolution.ToDouble(); + case TiffCompression.None: + { + this.CompressionType = TiffCompressionType.None; + break; + } + + default: + { + throw new NotSupportedException("The specified TIFF compression format is not supported."); + } + } + + TiffPhotometricInterpretation photometricInterpretation; + + if (ifd.TryGetIfdEntry(TiffTags.PhotometricInterpretation, out TiffIfdEntry photometricInterpretationEntry)) + { + photometricInterpretation = (TiffPhotometricInterpretation)this.ReadUnsignedInteger(ref photometricInterpretationEntry); + } + else + { + if (compression == TiffCompression.Ccitt1D) + { + photometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero; + } + else + { + throw new ImageFormatException("The TIFF photometric interpretation entry is missing."); + } + } + + switch (photometricInterpretation) + { + case TiffPhotometricInterpretation.WhiteIsZero: + { + if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry)) + { + uint[] bitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry); + + if (bitsPerSample.Length == 1 && bitsPerSample[0] == 8) + { + this.ColorType = TiffColorType.WhiteIsZero8; + } + else + { + throw new NotSupportedException("The specified TIFF bit-depth is not supported."); + } + } + else + { + throw new NotSupportedException("TIFF bilevel images are not supported."); + } + + break; + } + + default: + throw new NotSupportedException("The specified TIFF photometric interpretation is not supported."); + } + } + + /// + /// Calculates the size (in bytes) for a pixel buffer using the determined color format. + /// + /// The width for the desired pixel buffer. + /// The height for the desired pixel buffer. + /// The size (in bytes) of the required pixel buffer. + public int CalculateImageBufferSize(int width, int height) + { + switch (this.ColorType) + { + case TiffColorType.WhiteIsZero8: + return width * height; + default: + throw new InvalidOperationException(); + } + } + + /// + /// Decompresses an image block from the input stream into the specified buffer. + /// + /// The offset within the file of the image block. + /// The size (in bytes) of the compressed data. + /// The buffer to write the uncompressed data. + public void DecompressImageBlock(uint offset, uint byteCount, byte[] buffer) + { + this.InputStream.Seek(offset, SeekOrigin.Begin); + + switch (this.CompressionType) + { + case TiffCompressionType.None: + NoneTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); + break; + default: + throw new InvalidOperationException(); + } + } + + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The pixel format. + /// The buffer to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public void ProcessImageBlock(byte[] data, PixelAccessor pixels, int left, int top, int width, int height) + where TColor : struct, IPixel + { + switch (this.ColorType) + { + case TiffColorType.WhiteIsZero8: + WhiteIsZero8TiffColor.Decode(data, pixels, left, top, width, height); + break; + default: + throw new InvalidOperationException(); } } @@ -207,7 +384,7 @@ namespace ImageSharp.Formats this.InputStream.Seek(offset, SeekOrigin.Begin); byte[] data = new byte[byteLength]; - this.ReadBytes(data, (int)byteLength); + this.InputStream.ReadFull(data, (int)byteLength); entry.Value = data; } @@ -621,29 +798,6 @@ namespace ImageSharp.Formats } } - /// - /// Reads a sequence of bytes from the input stream into a buffer. - /// - /// A buffer to store the retrieved data. - /// The number of bytes to read. - private void ReadBytes(byte[] buffer, int count) - { - int offset = 0; - - while (count > 0) - { - int bytesRead = this.InputStream.Read(buffer, offset, count); - - if (bytesRead == 0) - { - break; - } - - offset += bytesRead; - count -= bytesRead; - } - } - /// /// Converts buffer data into an using the correct endianness. /// diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs new file mode 100644 index 000000000..e4049cf0f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageSharp.Formats +{ + using System.IO; + + /// + /// TIFF specific utilities and extension methods. + /// + internal static class TiffUtils + { + /// + /// Reads a sequence of bytes from the input stream into a buffer. + /// + /// The stream to read from. + /// A buffer to store the retrieved data. + /// The number of bytes to read. + public static void ReadFull(this Stream stream, byte[] buffer, int count) + { + int offset = 0; + + while (count > 0) + { + int bytesRead = stream.Read(buffer, offset, count); + + if (bytesRead == 0) + { + break; + } + + offset += bytesRead; + count -= bytesRead; + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs new file mode 100644 index 000000000..e3277eb96 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.IO; + using Xunit; + + using ImageSharp.Formats; + + public class NoneTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 8, new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 })] + [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 5, new byte[] { 10, 15, 20, 25, 30 })] + public void Decompress_ReadsData(byte[] inputData, int byteCount, byte[] expectedResult) + { + Stream stream = new MemoryStream(inputData); + byte[] buffer = new byte[expectedResult.Length]; + + NoneTiffCompression.Decompress(stream, byteCount, buffer); + + Assert.Equal(expectedResult, buffer); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs new file mode 100644 index 000000000..7fdb12177 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using Xunit; + + public abstract class PhotometricInterpretationTestBase + { + public static Color[][] Offset(Color[][] input, int xOffset, int yOffset, int width, int height) + { + int inputHeight = input.Length; + int inputWidth = input[0].Length; + + Color[][] output = new Color[height][]; + + for (int y = 0; y < output.Length; y++) + { + output[y] = new Color[width]; + } + + for (int y = 0; y < inputHeight; y++) + { + for (int x = 0; x < inputWidth; x++) + { + output[y + yOffset][x + xOffset] = input[y][x]; + } + } + + return output; + } + + public static void AssertDecode(Color[][] expectedResult, Action> decodeAction) + { + int resultWidth = expectedResult[0].Length; + int resultHeight = expectedResult.Length; + Image image = new Image(resultWidth, resultHeight); + + using (PixelAccessor pixels = image.Lock()) + { + decodeAction(pixels); + } + + using (PixelAccessor pixels = image.Lock()) + { + for (int y = 0; y < resultHeight; y++) + { + for (int x = 0; x < resultWidth; x++) + { + Assert.Equal(expectedResult[y][x], pixels[x, y]); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs new file mode 100644 index 000000000..075881f61 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.Collections.Generic; + using Xunit; + + using ImageSharp.Formats; + + public class WhiteIsZero8TiffColorTests : PhotometricInterpretationTestBase + { + private static Color Gray000 = new Color(255, 255, 255, 255); + private static Color Gray128 = new Color(127, 127, 127, 255); + private static Color Gray255 = new Color(0, 0, 0, 255); + + private static byte[] GrayscaleBytes4x4 = new byte[] { 128, 255, 000, 255, + 255, 255, 255, 255, + 000, 128, 128, 255, + 255, 000, 255, 128 }; + + private static Color[][] GrayscaleResult4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 }, + new[] { Gray255, Gray255, Gray255, Gray255 }, + new[] { Gray000, Gray128, Gray128, Gray255 }, + new[] { Gray255, Gray000, Gray255, Gray128 }}; + + public static IEnumerable DecodeData + { + get + { + yield return new object[] { GrayscaleBytes4x4, 0, 0, 4, 4, GrayscaleResult4x4 }; + yield return new object[] { GrayscaleBytes4x4, 0, 0, 4, 4, Offset(GrayscaleResult4x4, 0, 0, 6, 6) }; + yield return new object[] { GrayscaleBytes4x4, 1, 0, 4, 4, Offset(GrayscaleResult4x4, 1, 0, 6, 6) }; + yield return new object[] { GrayscaleBytes4x4, 0, 1, 4, 4, Offset(GrayscaleResult4x4, 0, 1, 6, 6) }; + yield return new object[] { GrayscaleBytes4x4, 1, 1, 4, 4, Offset(GrayscaleResult4x4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(DecodeData))] + public void Decode_WritesPixelData(byte[] inputData, int left, int top, int width, int height, Color[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + WhiteIsZero8TiffColor.Decode(inputData, pixels, left, top, width, height); + }); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index b0a97102f..34a0c2e4c 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -5,6 +5,7 @@ namespace ImageSharp.Tests { + using System; using System.IO; using Xunit; @@ -39,7 +40,7 @@ namespace ImageSharp.Tests [Theory] [InlineData(false, 150u, 1u, 200u, 1u, 2u /* Inch */, 150.0, 200.0)] - [InlineData(false, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 / 2.54, 200.0 / 2.54)] + [InlineData(false, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 * 2.54, 200.0 * 2.54)] [InlineData(false, 150u, 1u, 200u, 1u, 1u /* None */, 96.0, 96.0)] [InlineData(false, 150u, 1u, 200u, 1u, null /* Inch */, 150.0, 200.0)] [InlineData(false, 5u, 2u, 9u, 4u, 2u /* Inch */, 2.5, 2.25)] @@ -47,9 +48,9 @@ namespace ImageSharp.Tests [InlineData(false, 150u, 1u, null, null, 2u /* Inch */, 150.0, 96.0)] [InlineData(false, null, null, 200u, 1u, 2u /* Inch */, 96.0, 200.0)] [InlineData(true, 150u, 1u, 200u, 1u, 2u /* Inch */, 150.0, 200.0)] - [InlineData(true, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 / 2.54, 200.0 / 2.54)] + [InlineData(true, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 * 2.54, 200.0 * 2.54)] [InlineData(true, 150u, 1u, 200u, 1u, 1u /* None */, 96.0, 96.0)] - [InlineData(false, 5u, 2u, 9u, 4u, 2u /* Inch */, 2.5, 2.25)] + [InlineData(true, 5u, 2u, 9u, 4u, 2u /* Inch */, 2.5, 2.25)] [InlineData(true, 150u, 1u, 200u, 1u, null /* Inch */, 150.0, 200.0)] [InlineData(true, null, null, null, null, null /* Inch */, 96.0, 96.0)] [InlineData(true, 150u, 1u, null, null, 2u /* Inch */, 150.0, 96.0)] @@ -86,8 +87,8 @@ namespace ImageSharp.Tests decoder.DecodeImage(ifd, image); - Assert.Equal(expectedHorizonalResolution, image.MetaData.HorizontalResolution); - Assert.Equal(expectedVerticalResolution, image.MetaData.VerticalResolution); + Assert.Equal(expectedHorizonalResolution, image.MetaData.HorizontalResolution, 10); + Assert.Equal(expectedVerticalResolution, image.MetaData.VerticalResolution, 10); } [Theory] @@ -124,6 +125,180 @@ namespace ImageSharp.Tests Assert.Equal("The TIFF IFD does not specify the image dimensions.", e.Message); } + [Theory] + [InlineData(false, TiffCompression.None, TiffCompressionType.None)] + [InlineData(true, TiffCompression.None, TiffCompressionType.None)] + public void ReadImageFormat_DeterminesCorrectCompressionImplementation(bool isLittleEndian, ushort compression, int compressionType) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.Compression, TiffType.Short, compression)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); + + Assert.Equal((TiffCompressionType)compressionType, decoder.CompressionType); + } + + [Theory] + [InlineData(false, TiffCompression.Ccitt1D)] + [InlineData(false, TiffCompression.CcittGroup3Fax)] + [InlineData(false, TiffCompression.CcittGroup4Fax)] + [InlineData(false, TiffCompression.Deflate)] + [InlineData(false, TiffCompression.ItuTRecT43)] + [InlineData(false, TiffCompression.ItuTRecT82)] + [InlineData(false, TiffCompression.Jpeg)] + [InlineData(false, TiffCompression.Lzw)] + [InlineData(false, TiffCompression.OldDeflate)] + [InlineData(false, TiffCompression.OldJpeg)] + [InlineData(false, TiffCompression.PackBits)] + [InlineData(false, 999)] + [InlineData(true, TiffCompression.Ccitt1D)] + [InlineData(true, TiffCompression.CcittGroup3Fax)] + [InlineData(true, TiffCompression.CcittGroup4Fax)] + [InlineData(true, TiffCompression.Deflate)] + [InlineData(true, TiffCompression.ItuTRecT43)] + [InlineData(true, TiffCompression.ItuTRecT82)] + [InlineData(true, TiffCompression.Jpeg)] + [InlineData(true, TiffCompression.Lzw)] + [InlineData(true, TiffCompression.OldDeflate)] + [InlineData(true, TiffCompression.OldJpeg)] + [InlineData(true, TiffCompression.PackBits)] + [InlineData(true, 999)] + public void ReadImageFormat_ThrowsExceptionForUnsupportedCompression(bool isLittleEndian, ushort compression) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.Compression, TiffType.Short, compression)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + + var e = Assert.Throws(() => decoder.ReadImageFormat(ifd)); + + Assert.Equal("The specified TIFF compression format is not supported.", e.Message); + } + + [Theory] + [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)] + [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)] + public void ReadImageFormat_DeterminesCorrectColorImplementation(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample, int colorType) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); + + Assert.Equal((TiffColorType)colorType, decoder.ColorType); + } + + // [Theory] + // [InlineData(false, new[] { 8 }, TiffColorType.WhiteIsZero8)] + // [InlineData(true, new[] { 8 }, TiffColorType.WhiteIsZero8)] + // public void ReadImageFormat_UsesDefaultColorImplementationForCcitt1D(bool isLittleEndian, int[] bitsPerSample, int colorType) + // { + // Stream stream = CreateTiffGenIfd() + // .WithEntry(TiffGenEntry.Integer(TiffTags.Compression, TiffType.Short, (int)TiffCompression.Ccitt1D)) + // .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample)) + // .WithoutEntry(TiffTags.PhotometricInterpretation) + // .ToStream(isLittleEndian); + + // TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + // TiffIfd ifd = decoder.ReadIfd(0); + // decoder.ReadImageFormat(ifd); + + // Assert.Equal((TiffColorType)colorType, decoder.ColorType); + // } + + [Theory] + [MemberData(nameof(IsLittleEndianValues))] + public void ReadImageFormat_ThrowsExceptionForMissingPhotometricInterpretation(bool isLittleEndian) + { + Stream stream = CreateTiffGenIfd() + .WithoutEntry(TiffTags.PhotometricInterpretation) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + + var e = Assert.Throws(() => decoder.ReadImageFormat(ifd)); + + Assert.Equal("The TIFF photometric interpretation entry is missing.", e.Message); + } + + [Theory] + [InlineData(false, TiffPhotometricInterpretation.BlackIsZero)] + [InlineData(false, TiffPhotometricInterpretation.CieLab)] + [InlineData(false, TiffPhotometricInterpretation.ColorFilterArray)] + [InlineData(false, TiffPhotometricInterpretation.IccLab)] + [InlineData(false, TiffPhotometricInterpretation.ItuLab)] + [InlineData(false, TiffPhotometricInterpretation.LinearRaw)] + [InlineData(false, TiffPhotometricInterpretation.PaletteColor)] + [InlineData(false, TiffPhotometricInterpretation.Rgb)] + [InlineData(false, TiffPhotometricInterpretation.Separated)] + [InlineData(false, TiffPhotometricInterpretation.TransparencyMask)] + [InlineData(false, TiffPhotometricInterpretation.YCbCr)] + [InlineData(false, 999)] + [InlineData(true, TiffPhotometricInterpretation.BlackIsZero)] + [InlineData(true, TiffPhotometricInterpretation.CieLab)] + [InlineData(true, TiffPhotometricInterpretation.ColorFilterArray)] + [InlineData(true, TiffPhotometricInterpretation.IccLab)] + [InlineData(true, TiffPhotometricInterpretation.ItuLab)] + [InlineData(true, TiffPhotometricInterpretation.LinearRaw)] + [InlineData(true, TiffPhotometricInterpretation.PaletteColor)] + [InlineData(true, TiffPhotometricInterpretation.Rgb)] + [InlineData(true, TiffPhotometricInterpretation.Separated)] + [InlineData(true, TiffPhotometricInterpretation.TransparencyMask)] + [InlineData(true, TiffPhotometricInterpretation.YCbCr)] + [InlineData(true, 999)] + public void ReadImageFormat_ThrowsExceptionForUnsupportedPhotometricInterpretation(bool isLittleEndian, ushort photometricInterpretation) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + + var e = Assert.Throws(() => decoder.ReadImageFormat(ifd)); + + Assert.Equal("The specified TIFF photometric interpretation is not supported.", e.Message); + } + + [Theory] + [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })] + [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })] + public void ReadImageFormat_ThrowsExceptionForUnsupportedBitDepth(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + + var e = Assert.Throws(() => decoder.ReadImageFormat(ifd)); + + Assert.Equal("The specified TIFF bit-depth is not supported.", e.Message); + } + + [Theory] + [InlineData(TiffColorType.WhiteIsZero8, 100, 80, 100 * 80)] + public void CalculateImageBufferSize_ReturnsCorrectSize(ushort colorType, int width, int height, int expectedResult) + { + TiffDecoderCore decoder = new TiffDecoderCore(null); + + int bufferSize = decoder.CalculateImageBufferSize(width, height); + + Assert.Equal(expectedResult, bufferSize); + } + private TiffGenIfd CreateTiffGenIfd() { return new TiffGenIfd() @@ -134,7 +309,10 @@ namespace ImageSharp.Tests TiffGenEntry.Integer(TiffTags.ImageLength, TiffType.Long, ImageHeight), TiffGenEntry.Rational(TiffTags.XResolution, XResolution, 1), TiffGenEntry.Rational(TiffTags.YResolution, YResolution, 1), - TiffGenEntry.Integer(TiffTags.ResolutionUnit, TiffType.Short, 2) + TiffGenEntry.Integer(TiffTags.ResolutionUnit, TiffType.Short, 2), + TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, (int)TiffPhotometricInterpretation.WhiteIsZero), + TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, new int[] { 8 }), + TiffGenEntry.Integer(TiffTags.Compression, TiffType.Short, (int)TiffCompression.None) } }; } diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs index c44291640..4b62b9803 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs @@ -14,7 +14,7 @@ namespace ImageSharp.Tests { public static TiffGenIfd WithoutEntry(this TiffGenIfd ifd, ushort tag) { - TiffGenEntry entry = ifd.Entries.First(e => e.Tag == tag); + TiffGenEntry entry = ifd.Entries.FirstOrDefault(e => e.Tag == tag); if (entry != null) { ifd.Entries.Remove(entry);