Skip to content

Commit 826512f

Browse files
Merge pull request #3230 from jafin/jf/tiff-default-bitspersample
Default a missing TIFF BitsPerSample to one bit per sample
2 parents 11af26c + 55bb574 commit 826512f

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
@@ -216,10 +216,32 @@ private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
216216
meta.EncodingWidth = GetImageWidth(profile);
217217
meta.EncodingHeight = GetImageHeight(profile);
218218

219-
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue)
220-
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
219+
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue))
221220
{
222-
meta.BitsPerSample = bitsPerSample;
221+
// TryParse returns zero sample depths on failure. A present invalid entry must not inherit the RGB defaults.
222+
meta.BitsPerSample = TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)
223+
? bitsPerSample
224+
: default;
225+
}
226+
else
227+
{
228+
// TIFF 6.0 defines one bit per sample when BitsPerSample is absent (page 29).
229+
// SamplesPerPixel defaults to one (page 39). These file defaults differ from new metadata's RGB defaults.
230+
int samplesPerPixel = profile.TryGetValue(ExifTag.SamplesPerPixel, out IExifValue<ushort>? samplesPerPixelValue)
231+
? samplesPerPixelValue.Value
232+
: 1;
233+
234+
// This metadata type stores at most four sample depths. Do not reduce a different count to a supported layout.
235+
if (samplesPerPixel is < 1 or > 4)
236+
{
237+
TiffThrowHelper.ThrowNotSupported("Only TIFF images with one to four samples per pixel are supported.");
238+
}
239+
240+
meta.BitsPerSample = new TiffBitsPerSample(
241+
1,
242+
(ushort)(samplesPerPixel > 1 ? 1 : 0),
243+
(ushort)(samplesPerPixel > 2 ? 1 : 0),
244+
(ushort)(samplesPerPixel > 3 ? 1 : 0));
223245
}
224246

225247
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
@@ -1006,6 +1006,78 @@ public void Decode_MalformedIccProfile_ThrowsWithStrict()
10061006
});
10071007
}
10081008

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

‎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)