Skip to content

Commit 2bcb59f

Browse files
Merge pull request #3254 from SixLabors/fix/tiff-default-bitspersample-v4
[V4] Apply TIFF sample-depth defaults without changing invalid layouts
2 parents 8864c94 + 33dfc63 commit 2bcb59f

12 files changed

Lines changed: 149 additions & 8 deletions

‎src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,6 @@ internal static class TiffConstants
6868
/// </summary>
6969
public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;
7070

71-
/// <summary>
72-
/// The default bits per sample for color images with 8 bits for each color channel.
73-
/// </summary>
74-
public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;
75-
7671
/// <summary>
7772
/// The default compression is None.
7873
/// </summary>
@@ -108,6 +103,13 @@ internal static class TiffConstants
108103
/// </summary>
109104
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8);
110105

106+
// Initialize this alias after the value it references. Static fields initialize in declaration order.
107+
108+
/// <summary>
109+
/// The default bits per sample for color images with 8 bits for each color channel.
110+
/// </summary>
111+
public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;
112+
111113
/// <summary>
112114
/// The list of mime types that equate to a tiff.
113115
/// </summary>

‎src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,32 @@ private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
160160
meta.EncodingWidth = GetImageWidth(profile);
161161
meta.EncodingHeight = GetImageHeight(profile);
162162

163-
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue)
164-
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
163+
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue))
165164
{
166-
meta.BitsPerSample = bitsPerSample;
165+
// TryParse returns zero sample depths on failure. A present invalid entry must not inherit the RGB defaults.
166+
meta.BitsPerSample = TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)
167+
? bitsPerSample
168+
: default;
169+
}
170+
else
171+
{
172+
// TIFF 6.0 defines one bit per sample when BitsPerSample is absent (page 29).
173+
// SamplesPerPixel defaults to one (page 39). These file defaults differ from new metadata's RGB defaults.
174+
int samplesPerPixel = profile.TryGetValue(ExifTag.SamplesPerPixel, out IExifValue<ushort>? samplesPerPixelValue)
175+
? samplesPerPixelValue.Value
176+
: 1;
177+
178+
// This metadata type stores at most four sample depths. Do not reduce a different count to a supported layout.
179+
if (samplesPerPixel is < 1 or > 4)
180+
{
181+
TiffThrowHelper.ThrowNotSupported("Only TIFF images with one to four samples per pixel are supported.");
182+
}
183+
184+
meta.BitsPerSample = new TiffBitsPerSample(
185+
1,
186+
(ushort)(samplesPerPixel > 1 ? 1 : 0),
187+
(ushort)(samplesPerPixel > 2 ? 1 : 0),
188+
(ushort)(samplesPerPixel > 3 ? 1 : 0));
167189
}
168190

169191
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();

‎tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,78 @@ public void Decode_MalformedIccProfile_ThrowsWithStrict()
995995
});
996996
}
997997

998+
/// <summary>
999+
/// Missing sample-depth tags use the TIFF default in Decode and Identify under every integrity mode.
1000+
/// </summary>
1001+
/// <param name="path">The stored bilevel TIFF without BitsPerSample, with or without SamplesPerPixel.</param>
1002+
[Theory]
1003+
[InlineData(BilevelWithoutBitsPerSampleUncompressed)]
1004+
[InlineData(BilevelWithoutBitsPerSampleFax4)]
1005+
[InlineData(BilevelWithoutSampleTags)]
1006+
public void TiffDecoder_CanDecode_BilevelWithoutBitsPerSample(string path)
1007+
{
1008+
using Image<L8> expected = Image.Load<L8>(TestFile.Create(Fax4Compressed).Bytes);
1009+
byte[] data = TestFile.Create(path).Bytes;
1010+
1011+
foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
1012+
{
1013+
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
1014+
using Image<L8> actual = Image.Load<L8>(options, data);
1015+
ImageInfo info = Image.Identify(options, data);
1016+
1017+
ImageComparer.Exact.VerifySimilarity(expected, actual);
1018+
TiffFrameMetadata metadata = actual.Frames.RootFrame.Metadata.GetTiffMetadata();
1019+
Assert.Equal(new TiffBitsPerSample(1, 0, 0), metadata.BitsPerSample);
1020+
Assert.Equal(TiffBitsPerPixel.Bit1, metadata.BitsPerPixel);
1021+
Assert.Equal(1, info.PixelType.BitsPerPixel);
1022+
Assert.Equal(TiffBitsPerPixel.Bit1, info.FrameMetadataCollection[0].GetTiffMetadata().BitsPerPixel);
1023+
}
1024+
}
1025+
1026+
/// <summary>
1027+
/// A missing sample-depth tag must not reduce an unsupported sample count to a supported layout.
1028+
/// </summary>
1029+
/// <param name="path">The stored TIFF with zero or five declared samples per pixel.</param>
1030+
[Theory]
1031+
[InlineData(MissingBitsPerSampleWithZeroSamples)]
1032+
[InlineData(MissingBitsPerSampleWithFiveSamples)]
1033+
public void TiffDecoder_WithoutBitsPerSample_RejectsUnsupportedSampleCounts(string path)
1034+
{
1035+
byte[] data = TestFile.Create(path).Bytes;
1036+
1037+
foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
1038+
{
1039+
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
1040+
Assert.Throws<NotSupportedException>(() => Image.Identify(options, data));
1041+
Assert.Throws<NotSupportedException>(() =>
1042+
{
1043+
using Image<L8> image = Image.Load<L8>(options, data);
1044+
});
1045+
}
1046+
}
1047+
1048+
/// <summary>
1049+
/// A present but unusable sample-depth tag must not inherit a valid default layout.
1050+
/// </summary>
1051+
/// <param name="path">The stored TIFF with an empty or zero-valued BitsPerSample entry.</param>
1052+
[Theory]
1053+
[InlineData(EmptyBitsPerSample)]
1054+
[InlineData(ZeroBitsPerSample)]
1055+
public void TiffDecoder_PresentInvalidBitsPerSample_IsNotDefaulted(string path)
1056+
{
1057+
byte[] data = TestFile.Create(path).Bytes;
1058+
1059+
foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
1060+
{
1061+
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
1062+
Assert.Throws<NotSupportedException>(() =>
1063+
{
1064+
using Image<L8> image = Image.Load<L8>(options, data);
1065+
});
1066+
}
1067+
}
1068+
9981069
private static byte[] CreateTiffWithMalformedIccProfile()
9991070
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder());
1071+
10001072
}

‎tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ private class NumberComparer : IEqualityComparer<Number>
2727
public int GetHashCode(Number obj) => obj.GetHashCode();
2828
}
2929

30+
/// <summary>
31+
/// New image and frame metadata use eight-bit RGB, independently of the defaults for absent file tags.
32+
/// </summary>
33+
[Fact]
34+
public void TiffMetadata_DefaultsToEightBitRgb()
35+
{
36+
TiffMetadata imageMetadata = new();
37+
TiffFrameMetadata frameMetadata = new();
38+
TiffBitsPerSample expected = new(8, 8, 8);
39+
40+
Assert.Equal(expected, imageMetadata.BitsPerSample);
41+
Assert.Equal(expected, frameMetadata.BitsPerSample);
42+
Assert.Equal(TiffBitsPerPixel.Bit24, imageMetadata.BitsPerPixel);
43+
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetadata.BitsPerPixel);
44+
}
45+
3046
[Fact]
3147
public void TiffMetadata_CloneIsDeep()
3248
{

‎tests/ImageSharp.Tests/TestImages.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,14 @@ public static class Tiff
10021002
public const string Benchmark_RgbPackbits = "medium_rgb_packbits.tiff";
10031003
public const string Benchmark_RgbUncompressed = "medium_rgb_uncompressed.tiff";
10041004

1005+
public const string BilevelWithoutBitsPerSampleUncompressed = "Tiff/defaults/bilevel-no-bitspersample-none.tiff";
1006+
public const string BilevelWithoutBitsPerSampleFax4 = "Tiff/defaults/bilevel-no-bitspersample-g4.tiff";
1007+
public const string BilevelWithoutSampleTags = "Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff";
1008+
public const string MissingBitsPerSampleWithZeroSamples = "Tiff/defaults/no-bitspersample-samplesperpixel-zero.tiff";
1009+
public const string MissingBitsPerSampleWithFiveSamples = "Tiff/defaults/no-bitspersample-samplesperpixel-five.tiff";
1010+
public const string EmptyBitsPerSample = "Tiff/defaults/empty-bitspersample.tiff";
1011+
public const string ZeroBitsPerSample = "Tiff/defaults/zero-bitspersample.tiff";
1012+
10051013
public const string Calliphora_GrayscaleUncompressed = "Tiff/Calliphora_grayscale_uncompressed.tiff";
10061014
public const string Calliphora_GrayscaleDeflate_Predictor = "Tiff/Calliphora_gray_deflate_predictor.tiff";
10071015
public const string Calliphora_GrayscaleLzw_Predictor = "Tiff/Calliphora_gray_lzw_predictor.tiff";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e25f990d596ef9f0a50a7e891dbda11d9912f61f6f9c5fc67aa6a6bb26ddda2e
3+
size 468
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:27acb91aec7b5e4a90602129bb17f04bd149d5ab55564d09ba298a95a1a828cf
3+
size 440
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:b4d3e75163b6fc2afb689501a6a249f22ab8029012786d9a8917173f591fbd43
3+
size 428
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:392157cdd816ce57ef33aeb585c70d27d20cd31364807cb9750bea09495943f1
3+
size 440
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:878be6433aeebcfe1f69d2099cf0186b1d48a79306c3aa5034122e094d808e0c
3+
size 590

0 commit comments

Comments
 (0)