Repository navigation
TIFF without a BitsPerSample entry fails to decode or reports 0 bits per pixel #3242
Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
ImageSharp version
main @ a73c43d (also present in v4.1.3, same code)
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 Pro 10.0.22631, x64
.NET Framework version
.NET 10 (net10.0)
Description
TIFF 6.0 gives BitsPerSample a default of 1 (one bit for each sample) when the entry is absent. Bilevel fax and scanner files often leave the entry out. ImageSharp does not use this default:
- An uncompressed bilevel TIFF without BitsPerSample fails to decode with
NotSupportedException: The number of samples in the TIFF BitsPerSample entry is not supported. - A CCITT Group 4 bilevel TIFF without BitsPerSample decodes, but its frame metadata reports
BitsPerPixel0 andBitsPerSample(0, 0, 0). Image.IdentifyreportsPixelType.BitsPerPixel0 for both files.
Expected: the decoder uses 1 bit for each sample (SamplesPerPixel itself defaults to 1). Both files then decode to the same pixels as the same file with an explicit BitsPerSample = 1, and the metadata and Identify report 1 bit per pixel.
Cause: TiffFrameMetadata.Parse sets BitsPerSample only when the entry is present and valid. When the entry is absent, the property keeps its initializer value TiffConstants.DefaultBitsPerSample. That value is meant to be 8-bit RGB. It is actually (0, 0, 0) at run time, because DefaultBitsPerSample = BitsPerSampleRgb8Bit is declared before BitsPerSampleRgb8Bit, and static fields initialize in declaration order. The decoder then rejects the zero-sample value, or reports 0 bits per pixel. For CCITT compression, the decoder options use 1 bit per sample, so those pixels decode correctly, but the frame metadata is not updated.
A fix is proposed in #3230.
Related: #2587 (closed) reported the same exception for CCITT G4 files. That fix forced 1 bit per sample for CCITT compression only, so uncompressed files without BitsPerSample still fail, and the metadata and Identify still report 0 bits per pixel.
Steps to Reproduce
The snippet encodes a 16x8 bilevel TIFF, then renames the BitsPerSample entry (tag 258) to an unused private tag (65000). The decoder then does not find the entry.
using System.Buffers.Binary;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
foreach (TiffCompression compression in new[] { TiffCompression.None, TiffCompression.CcittGroup4Fax })
{
using Image<L8> source = new(16, 8);
for (int y = 0; y < source.Height; y++)
{
for (int x = 0; x < source.Width; x++)
{
source[x, y] = new L8((byte)((x + y) % 3 == 0 ? 255 : 0));
}
}
using MemoryStream ms = new();
source.Save(ms, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit1, Compression = compression });
byte[] tiff = ms.ToArray();
// Rename the BitsPerSample entry (tag 258) of the first IFD to the unused tag 65000.
int ifd = (int)BinaryPrimitives.ReadUInt32LittleEndian(tiff.AsSpan(4));
int count = BinaryPrimitives.ReadUInt16LittleEndian(tiff.AsSpan(ifd));
for (int i = 0; i < count; i++)
{
Span<byte> entry = tiff.AsSpan(ifd + 2 + (i * 12));
if (BinaryPrimitives.ReadUInt16LittleEndian(entry) == 258)
{
BinaryPrimitives.WriteUInt16LittleEndian(entry, 65000);
}
}
Console.WriteLine($"{compression}: Identify BitsPerPixel = {Image.Identify(tiff).PixelType.BitsPerPixel}");
try
{
using Image<L8> image = Image.Load<L8>(tiff);
TiffFrameMetadata meta = image.Frames.RootFrame.Metadata.GetTiffMetadata();
Console.WriteLine($"{compression}: decoded, BitsPerPixel = {meta.BitsPerPixel}, BitsPerSample = {meta.BitsPerSample}");
}
catch (Exception ex)
{
Console.WriteLine($"{compression}: {ex.GetType().FullName}: {ex.Message}");
}
}Actual output:
None: Identify BitsPerPixel = 0
None: System.NotSupportedException: The number of samples in the TIFF BitsPerSample entry is not supported.
CcittGroup4Fax: Identify BitsPerPixel = 0
CcittGroup4Fax: decoded, BitsPerPixel = 0, BitsPerSample = TiffBitsPerSample(0, 0, 0)
Stack trace for the uncompressed file:
System.NotSupportedException: The number of samples in the TIFF BitsPerSample entry is not supported.
at SixLabors.ImageSharp.Formats.Tiff.TiffThrowHelper.ThrowNotSupported(String message)
at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderOptionsParser.ParseColorType(TiffDecoderCore options, ExifProfile exifProfile)
at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderOptionsParser.VerifyAndParse(TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata)
at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderCore.DecodeFrame[TPixel](ExifProfile tags, Nullable`1 size, CancellationToken cancellationToken)
at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderCore.Decode[TPixel](BufferedReadStream stream, CancellationToken cancellationToken)
at SixLabors.ImageSharp.Formats.ImageDecoderCore.Decode[TPixel](Configuration configuration, Stream stream, CancellationToken cancellationToken)
Expected: both files decode to the same pixels as the unmodified file. Identify and the frame metadata report 1 bit per pixel (TiffBitsPerPixel.Bit1).
Images
No images needed; the repro builds the input in memory.
Activity