Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@ internal static class TiffConstants
/// </summary>
public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;

/// <summary>
/// The default bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;

/// <summary>
/// The default compression is None.
/// </summary>
Expand Down Expand Up @@ -108,6 +103,13 @@ internal static class TiffConstants
/// </summary>
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8);

// Initialize this alias after the value it references. Static fields initialize in declaration order.

/// <summary>
/// The default bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;

/// <summary>
/// The list of mime types that equate to a tiff.
/// </summary>
Expand Down
28 changes: 25 additions & 3 deletions src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ushort[]>? bitsPerSampleValue)
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? 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<ushort>? 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();
Expand Down
72 changes: 72 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,78 @@ public void Decode_MalformedIccProfile_ThrowsWithStrict()
});
}

/// <summary>
/// Missing sample-depth tags use the TIFF default in Decode and Identify under every integrity mode.
/// </summary>
/// <param name="path">The stored bilevel TIFF without BitsPerSample, with or without SamplesPerPixel.</param>
[Theory]
[InlineData(BilevelWithoutBitsPerSampleUncompressed)]
[InlineData(BilevelWithoutBitsPerSampleFax4)]
[InlineData(BilevelWithoutSampleTags)]
public void TiffDecoder_CanDecode_BilevelWithoutBitsPerSample(string path)
{
using Image<L8> expected = Image.Load<L8>(TestFile.Create(Fax4Compressed).Bytes);
byte[] data = TestFile.Create(path).Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
using Image<L8> actual = Image.Load<L8>(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);
}
}

/// <summary>
/// A missing sample-depth tag must not reduce an unsupported sample count to a supported layout.
/// </summary>
/// <param name="path">The stored TIFF with zero or five declared samples per pixel.</param>
[Theory]
[InlineData(MissingBitsPerSampleWithZeroSamples)]
[InlineData(MissingBitsPerSampleWithFiveSamples)]
public void TiffDecoder_WithoutBitsPerSample_RejectsUnsupportedSampleCounts(string path)
{
byte[] data = TestFile.Create(path).Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
Assert.Throws<NotSupportedException>(() => Image.Identify(options, data));
Assert.Throws<NotSupportedException>(() =>
{
using Image<L8> image = Image.Load<L8>(options, data);
});
}
}

/// <summary>
/// A present but unusable sample-depth tag must not inherit a valid default layout.
/// </summary>
/// <param name="path">The stored TIFF with an empty or zero-valued BitsPerSample entry.</param>
[Theory]
[InlineData(EmptyBitsPerSample)]
[InlineData(ZeroBitsPerSample)]
public void TiffDecoder_PresentInvalidBitsPerSample_IsNotDefaulted(string path)
{
byte[] data = TestFile.Create(path).Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
Assert.Throws<NotSupportedException>(() =>
{
using Image<L8> image = Image.Load<L8>(options, data);
});
}
}

private static byte[] CreateTiffWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder());

}
16 changes: 16 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ private class NumberComparer : IEqualityComparer<Number>
public int GetHashCode(Number obj) => obj.GetHashCode();
}

/// <summary>
/// New image and frame metadata use eight-bit RGB, independently of the defaults for absent file tags.
/// </summary>
[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()
{
Expand Down
8 changes: 8 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/defaults/empty-bitspersample.tiff
Git LFS file not shown
Git LFS file not shown
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/defaults/zero-bitspersample.tiff
Git LFS file not shown
Loading