diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
index be9e14df40..b86e179595 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
@@ -10,6 +10,11 @@ namespace ImageSharp.Formats.Tiff
///
internal enum TiffColorType
{
+ ///
+ /// Grayscale: 0 is imaged as white. The maximum value is imaged as black.
+ ///
+ WhiteIsZero,
+
///
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for bilevel images.
///
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs
new file mode 100644
index 0000000000..876ea87890
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs
@@ -0,0 +1,53 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats.Tiff
+{
+ using System;
+ using System.Numerics;
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+ using ImageSharp.PixelFormats;
+
+ ///
+ /// Implements the 'WhiteIsZero' photometric interpretation (for all bit depths).
+ ///
+ internal static class WhiteIsZeroTiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffer to read image data from.
+ /// The number of bits per sample for each pixel.
+ /// The image buffer to write pixels to.
+ /// The x-coordinate of the left-hand side of the image block.
+ /// The y-coordinate of the top of the image block.
+ /// The width of the image block.
+ /// The height of the image block.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decode(byte[] data, uint[] bitsPerSample, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default(TPixel);
+
+ BitReader bitReader = new BitReader(data);
+ float factor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x++)
+ {
+ int value = bitReader.ReadBits(bitsPerSample[0]);
+ float intensity = 1.0f - (((float)value) / factor);
+ color.PackFromVector4(new Vector4(intensity, intensity, intensity, 1.0f));
+ pixels[x, y] = color;
+ }
+
+ bitReader.NextRow();
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 225bddb1ed..98635eca7e 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -52,6 +52,11 @@ namespace ImageSharp.Formats
this.IsLittleEndian = isLittleEndian;
}
+ ///
+ /// Gets or sets the number of bits for each sample of the pixel format used to encode the image.
+ ///
+ public uint[] BitsPerSample { get; set; }
+
///
/// Gets or sets the photometric interpretation implementation to use when decoding the image.
///
@@ -288,11 +293,11 @@ namespace ImageSharp.Formats
{
if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry))
{
- uint[] bitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry);
+ this.BitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry);
- if (bitsPerSample.Length == 1)
+ if (this.BitsPerSample.Length == 1)
{
- switch (bitsPerSample[0])
+ switch (this.BitsPerSample[0])
{
case 8:
{
@@ -314,7 +319,8 @@ namespace ImageSharp.Formats
default:
{
- throw new NotSupportedException("The specified TIFF bit-depth is not supported.");
+ this.ColorType = TiffColorType.WhiteIsZero;
+ break;
}
}
}
@@ -322,6 +328,7 @@ namespace ImageSharp.Formats
else
{
this.ColorType = TiffColorType.WhiteIsZero1;
+ this.BitsPerSample = new[] { 1u };
}
break;
@@ -340,17 +347,14 @@ namespace ImageSharp.Formats
/// The size (in bytes) of the required pixel buffer.
public int CalculateImageBufferSize(int width, int height)
{
- switch (this.ColorType)
+ uint bitsPerPixel = 0;
+ for (int i = 0; i < this.BitsPerSample.Length; i++)
{
- case TiffColorType.WhiteIsZero1:
- return ((width + 7) / 8) * height;
- case TiffColorType.WhiteIsZero4:
- return ((width + 1) / 2) * height;
- case TiffColorType.WhiteIsZero8:
- return width * height;
- default:
- throw new InvalidOperationException();
+ bitsPerPixel += this.BitsPerSample[i];
}
+
+ int bytesPerRow = ((width * (int)bitsPerPixel) + 7) / 8;
+ return bytesPerRow * height;
}
///
@@ -391,6 +395,9 @@ namespace ImageSharp.Formats
{
switch (this.ColorType)
{
+ case TiffColorType.WhiteIsZero:
+ WhiteIsZeroTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height);
+ break;
case TiffColorType.WhiteIsZero1:
WhiteIsZero1TiffColor.Decode(data, pixels, left, top, width, height);
break;
diff --git a/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs
new file mode 100644
index 0000000000..f330690f9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs
@@ -0,0 +1,65 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+namespace ImageSharp.Formats.Tiff
+{
+ using System.IO;
+
+ ///
+ /// Utility class to read a sequence of bits from an array
+ ///
+ internal class BitReader
+ {
+ private readonly byte[] array;
+ private int offset;
+ private int bitOffset;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The array to read data from.
+ public BitReader(byte[] array)
+ {
+ this.array = array;
+ }
+
+ ///
+ /// Reads the specified number of bits from the array.
+ ///
+ /// The number of bits to read.
+ /// The value read from the array.
+ public int ReadBits(uint bits)
+ {
+ int value = 0;
+
+ for (uint i = 0; i < bits; i++)
+ {
+ int bit = (this.array[this.offset] >> (7 - this.bitOffset)) & 0x01;
+ value = (value << 1) | bit;
+
+ this.bitOffset++;
+
+ if (this.bitOffset == 8)
+ {
+ this.bitOffset = 0;
+ this.offset++;
+ }
+ }
+
+ return value;
+ }
+
+ ///
+ /// Moves the reader to the next row of byte-aligned data.
+ ///
+ public void NextRow()
+ {
+ if (this.bitOffset > 0)
+ {
+ this.bitOffset = 0;
+ this.offset++;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs
index ce04e0225a..e9d9556bd7 100644
--- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs
@@ -119,6 +119,18 @@ namespace ImageSharp.Tests
}
}
+ [Theory]
+ [MemberData(nameof(Bilevel_Data))]
+ [MemberData(nameof(Grayscale4_Data))]
+ [MemberData(nameof(Grayscale8_Data))]
+ public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
+ {
+ AssertDecode(expectedResult, pixels =>
+ {
+ WhiteIsZeroTiffColor.Decode(inputData, new[] { (uint)bitsPerSample }, pixels, left, top, width, height);
+ });
+ }
+
[Theory]
[MemberData(nameof(Bilevel_Data))]
public void Decode_WritesPixelData_Bilevel(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs
index 2cf6384597..168ecf0bc2 100644
--- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs
@@ -176,6 +176,8 @@ namespace ImageSharp.Tests
}
[Theory]
+ [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 }, TiffColorType.WhiteIsZero)]
+ [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 }, TiffColorType.WhiteIsZero)]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)]
[InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 4 }, TiffColorType.WhiteIsZero4)]
@@ -287,33 +289,57 @@ namespace ImageSharp.Tests
}
[Theory]
- [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })]
- [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })]
- public void ReadImageFormat_ThrowsExceptionForUnsupportedBitDepth(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample)
+ [InlineData(false, new[] { 8u })]
+ [InlineData(true, new[] { 8u })]
+ [InlineData(false, new[] { 4u })]
+ [InlineData(true, new[] { 4u })]
+ [InlineData(false, new[] { 1u })]
+ [InlineData(true, new[] { 1u })]
+ [InlineData(false, new[] { 1u, 2u, 3u })]
+ [InlineData(true, new[] { 1u, 2u, 3u })]
+ [InlineData(false, new[] { 8u, 8u, 8u })]
+ [InlineData(true, new[] { 8u, 8u, 8u })]
+ public void ReadImageFormat_ReadsBitsPerSample(bool isLittleEndian, uint[] bitsPerSample)
{
Stream stream = CreateTiffGenIfd()
- .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation))
.WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample))
.ToStream(isLittleEndian);
TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null);
TiffIfd ifd = decoder.ReadIfd(0);
+ decoder.ReadImageFormat(ifd);
- var e = Assert.Throws(() => decoder.ReadImageFormat(ifd));
+ Assert.Equal(bitsPerSample, decoder.BitsPerSample);
+ }
+
+ [Theory]
+ [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero)]
+ [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero)]
+ public void ReadImageFormat_ReadsBitsPerSample_DefaultsToBilevel(bool isLittleEndian, ushort photometricInterpretation)
+ {
+ Stream stream = CreateTiffGenIfd()
+ .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation))
+ .WithoutEntry(TiffTags.BitsPerSample)
+ .ToStream(isLittleEndian);
+
+ TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null);
+ TiffIfd ifd = decoder.ReadIfd(0);
+ decoder.ReadImageFormat(ifd);
- Assert.Equal("The specified TIFF bit-depth is not supported.", e.Message);
+ Assert.Equal(new[] { 1u }, decoder.BitsPerSample);
}
[Theory]
- [InlineData(TiffColorType.WhiteIsZero8, 100, 80, 100 * 80)]
- [InlineData(TiffColorType.WhiteIsZero4, 100, 80, 50 * 80)]
- [InlineData(TiffColorType.WhiteIsZero4, 99, 80, 50 * 80)]
- [InlineData(TiffColorType.WhiteIsZero1, 160, 80, 20 * 80)]
- [InlineData(TiffColorType.WhiteIsZero1, 153, 80, 20 * 80)]
- public void CalculateImageBufferSize_ReturnsCorrectSize(ushort colorType, int width, int height, int expectedResult)
+ [InlineData(new uint[] { 1 }, 160, 80, 20 * 80)]
+ [InlineData(new uint[] { 1 }, 153, 80, 20 * 80)]
+ [InlineData(new uint[] { 3 }, 100, 80, 38 * 80)]
+ [InlineData(new uint[] { 4 }, 100, 80, 50 * 80)]
+ [InlineData(new uint[] { 4 }, 99, 80, 50 * 80)]
+ [InlineData(new uint[] { 8 }, 100, 80, 100 * 80)]
+ public void CalculateImageBufferSize_ReturnsCorrectSize(uint[] bitsPerSample, int width, int height, int expectedResult)
{
TiffDecoderCore decoder = new TiffDecoderCore(null, null);
- decoder.ColorType = (TiffColorType)colorType;
+ decoder.BitsPerSample = bitsPerSample;
int bufferSize = decoder.CalculateImageBufferSize(width, height);