diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
index c24eee484b..6a41c05ca7 100644
--- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
+++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
@@ -68,11 +68,6 @@ internal static class TiffConstants
///
public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;
- ///
- /// The default bits per sample for color images with 8 bits for each color channel.
- ///
- public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;
-
///
/// The default compression is None.
///
@@ -108,6 +103,13 @@ internal static class TiffConstants
///
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8);
+ // Initialize this alias after the value it references. Static fields initialize in declaration order.
+
+ ///
+ /// The default bits per sample for color images with 8 bits for each color channel.
+ ///
+ public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;
+
///
/// The list of mime types that equate to a tiff.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
index d977b5f29f..d6daaef93e 100644
--- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
@@ -216,10 +216,32 @@ private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
meta.EncodingWidth = GetImageWidth(profile);
meta.EncodingHeight = GetImageHeight(profile);
- if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue)
- && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
+ if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue))
{
- meta.BitsPerSample = bitsPerSample;
+ // TryParse returns zero sample depths on failure. A present invalid entry must not inherit the RGB defaults.
+ meta.BitsPerSample = TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)
+ ? bitsPerSample
+ : default;
+ }
+ else
+ {
+ // TIFF 6.0 defines one bit per sample when BitsPerSample is absent (page 29).
+ // SamplesPerPixel defaults to one (page 39). These file defaults differ from new metadata's RGB defaults.
+ int samplesPerPixel = profile.TryGetValue(ExifTag.SamplesPerPixel, out IExifValue? samplesPerPixelValue)
+ ? samplesPerPixelValue.Value
+ : 1;
+
+ // This metadata type stores at most four sample depths. Do not reduce a different count to a supported layout.
+ if (samplesPerPixel is < 1 or > 4)
+ {
+ TiffThrowHelper.ThrowNotSupported("Only TIFF images with one to four samples per pixel are supported.");
+ }
+
+ meta.BitsPerSample = new TiffBitsPerSample(
+ 1,
+ (ushort)(samplesPerPixel > 1 ? 1 : 0),
+ (ushort)(samplesPerPixel > 2 ? 1 : 0),
+ (ushort)(samplesPerPixel > 3 ? 1 : 0));
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index bab79d8ecf..bc2595c98f 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -1006,6 +1006,78 @@ public void Decode_MalformedIccProfile_ThrowsWithStrict()
});
}
+ ///
+ /// Missing sample-depth tags use the TIFF default in Decode and Identify under every integrity mode.
+ ///
+ /// The stored bilevel TIFF without BitsPerSample, with or without SamplesPerPixel.
+ [Theory]
+ [InlineData(BilevelWithoutBitsPerSampleUncompressed)]
+ [InlineData(BilevelWithoutBitsPerSampleFax4)]
+ [InlineData(BilevelWithoutSampleTags)]
+ public void TiffDecoder_CanDecode_BilevelWithoutBitsPerSample(string path)
+ {
+ using Image expected = Image.Load(TestFile.Create(Fax4Compressed).Bytes);
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image actual = Image.Load(options, data);
+ ImageInfo info = Image.Identify(options, data);
+
+ ImageComparer.Exact.VerifySimilarity(expected, actual);
+ TiffFrameMetadata metadata = actual.Frames.RootFrame.Metadata.GetTiffMetadata();
+ Assert.Equal(new TiffBitsPerSample(1, 0, 0), metadata.BitsPerSample);
+ Assert.Equal(TiffBitsPerPixel.Bit1, metadata.BitsPerPixel);
+ Assert.Equal(1, info.PixelType.BitsPerPixel);
+ Assert.Equal(TiffBitsPerPixel.Bit1, info.FrameMetadataCollection[0].GetTiffMetadata().BitsPerPixel);
+ }
+ }
+
+ ///
+ /// A missing sample-depth tag must not reduce an unsupported sample count to a supported layout.
+ ///
+ /// The stored TIFF with zero or five declared samples per pixel.
+ [Theory]
+ [InlineData(MissingBitsPerSampleWithZeroSamples)]
+ [InlineData(MissingBitsPerSampleWithFiveSamples)]
+ public void TiffDecoder_WithoutBitsPerSample_RejectsUnsupportedSampleCounts(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ Assert.Throws(() => Image.Identify(options, data));
+ Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+ }
+ }
+
+ ///
+ /// A present but unusable sample-depth tag must not inherit a valid default layout.
+ ///
+ /// The stored TIFF with an empty or zero-valued BitsPerSample entry.
+ [Theory]
+ [InlineData(EmptyBitsPerSample)]
+ [InlineData(ZeroBitsPerSample)]
+ public void TiffDecoder_PresentInvalidBitsPerSample_IsNotDefaulted(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+ }
+ }
+
private static byte[] CreateTiffWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder());
+
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
index d181cacb5a..0fa6467ca9 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
@@ -27,6 +27,22 @@ private class NumberComparer : IEqualityComparer
public int GetHashCode(Number obj) => obj.GetHashCode();
}
+ ///
+ /// New image and frame metadata use eight-bit RGB, independently of the defaults for absent file tags.
+ ///
+ [Fact]
+ public void TiffMetadata_DefaultsToEightBitRgb()
+ {
+ TiffMetadata imageMetadata = new();
+ TiffFrameMetadata frameMetadata = new();
+ TiffBitsPerSample expected = new(8, 8, 8);
+
+ Assert.Equal(expected, imageMetadata.BitsPerSample);
+ Assert.Equal(expected, frameMetadata.BitsPerSample);
+ Assert.Equal(TiffBitsPerPixel.Bit24, imageMetadata.BitsPerPixel);
+ Assert.Equal(TiffBitsPerPixel.Bit24, frameMetadata.BitsPerPixel);
+ }
+
[Fact]
public void TiffMetadata_CloneIsDeep()
{
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index b15f7ae0fb..e4c3fffff6 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1002,6 +1002,14 @@ public static class Tiff
public const string Benchmark_RgbPackbits = "medium_rgb_packbits.tiff";
public const string Benchmark_RgbUncompressed = "medium_rgb_uncompressed.tiff";
+ public const string BilevelWithoutBitsPerSampleUncompressed = "Tiff/defaults/bilevel-no-bitspersample-none.tiff";
+ public const string BilevelWithoutBitsPerSampleFax4 = "Tiff/defaults/bilevel-no-bitspersample-g4.tiff";
+ public const string BilevelWithoutSampleTags = "Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff";
+ public const string MissingBitsPerSampleWithZeroSamples = "Tiff/defaults/no-bitspersample-samplesperpixel-zero.tiff";
+ public const string MissingBitsPerSampleWithFiveSamples = "Tiff/defaults/no-bitspersample-samplesperpixel-five.tiff";
+ public const string EmptyBitsPerSample = "Tiff/defaults/empty-bitspersample.tiff";
+ public const string ZeroBitsPerSample = "Tiff/defaults/zero-bitspersample.tiff";
+
public const string Calliphora_GrayscaleUncompressed = "Tiff/Calliphora_grayscale_uncompressed.tiff";
public const string Calliphora_GrayscaleDeflate_Predictor = "Tiff/Calliphora_gray_deflate_predictor.tiff";
public const string Calliphora_GrayscaleLzw_Predictor = "Tiff/Calliphora_gray_lzw_predictor.tiff";
diff --git a/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-g4.tiff b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-g4.tiff
new file mode 100644
index 0000000000..4ee08baf73
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-g4.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e25f990d596ef9f0a50a7e891dbda11d9912f61f6f9c5fc67aa6a6bb26ddda2e
+size 468
diff --git a/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-none.tiff b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-none.tiff
new file mode 100644
index 0000000000..7761945900
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-none.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27acb91aec7b5e4a90602129bb17f04bd149d5ab55564d09ba298a95a1a828cf
+size 440
diff --git a/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff
new file mode 100644
index 0000000000..1c37372676
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b4d3e75163b6fc2afb689501a6a249f22ab8029012786d9a8917173f591fbd43
+size 428
diff --git a/tests/Images/Input/Tiff/defaults/empty-bitspersample.tiff b/tests/Images/Input/Tiff/defaults/empty-bitspersample.tiff
new file mode 100644
index 0000000000..21189ebde7
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/empty-bitspersample.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:392157cdd816ce57ef33aeb585c70d27d20cd31364807cb9750bea09495943f1
+size 440
diff --git a/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-five.tiff b/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-five.tiff
new file mode 100644
index 0000000000..8b693687af
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-five.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:878be6433aeebcfe1f69d2099cf0186b1d48a79306c3aa5034122e094d808e0c
+size 590
diff --git a/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-zero.tiff b/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-zero.tiff
new file mode 100644
index 0000000000..b6cfe767f7
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/no-bitspersample-samplesperpixel-zero.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84caa139af2d4bc5b61b89fcc4c06a90f464c26c6c91d019bccd3836f51ddd16
+size 590
diff --git a/tests/Images/Input/Tiff/defaults/zero-bitspersample.tiff b/tests/Images/Input/Tiff/defaults/zero-bitspersample.tiff
new file mode 100644
index 0000000000..7ea562ed96
--- /dev/null
+++ b/tests/Images/Input/Tiff/defaults/zero-bitspersample.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fde04a56b5010542fdd79c3b55bf6bf93559fbc03e03197686036451c0691a92
+size 440